From 9f6a835653600858cc8b64e2555f210b47ec038d Mon Sep 17 00:00:00 2001 From: Sergey Nikitin Date: Tue, 17 Aug 2021 12:11:53 +0400 Subject: [PATCH 01/11] GRPC: Fixed change type of logical port --- .../openkilda/grpc/speaker/service/GrpcSenderService.java | 2 +- src-python/grpc-stub/server.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/service/GrpcSenderService.java b/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/service/GrpcSenderService.java index c309d86b34c..080b655287b 100644 --- a/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/service/GrpcSenderService.java +++ b/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/service/GrpcSenderService.java @@ -71,7 +71,7 @@ public GrpcSenderService(@Autowired NoviflowResponseMapper mapper) { */ public CompletableFuture createLogicalPort(String switchAddress, LogicalPortDto port) { try (GrpcSession session = makeSession(switchAddress)) { - session.setLogicalPort(port); + session.setLogicalPort(port).join(); return session.showConfigLogicalPort(port.getLogicalPortNumber()) .thenApply(portOptional -> portOptional .map(mapper::map) diff --git a/src-python/grpc-stub/server.py b/src-python/grpc-stub/server.py index 435a04eceb9..f3e3759259c 100644 --- a/src-python/grpc-stub/server.py +++ b/src-python/grpc-stub/server.py @@ -47,6 +47,11 @@ def SetLoginDetails(self, request, context): return noviflow_pb2.CliReply(reply_status=0) def SetConfigLogicalPort(self, request, context): + if request.logicalportno in self.storage.logical_ports: + port = self.storage.logical_ports[request.logicalportno] + if port.type != request.logicalporttype: + return noviflow_pb2.CliReply(reply_status=390) + port = LogicalPort(logical_port_number=request.logicalportno, name="novi_lport" + str(request.logicalportno), port_numbers=request.portno, From 1fabdf3fbc0875dace8dff47056eae31f9a1f336 Mon Sep 17 00:00:00 2001 From: Sergey Nikitin Date: Tue, 8 Feb 2022 18:44:23 +0400 Subject: [PATCH 02/11] Added sync switch on connect toggle --- .../model/system/FeatureTogglesDto.java | 7 ++++++- .../org/openkilda/model/KildaFeatureToggles.java | 16 ++++++++++++++-- .../ferma/frames/KildaFeatureTogglesFrame.java | 8 ++++++++ .../network/controller/sw/SwitchFsm.java | 8 +++++--- .../service/NetworkSwitchServiceTest.java | 7 +++++++ .../extension/env/EnvExtension.groovy | 1 + .../performancetests/BaseSpecification.groovy | 1 + 7 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/model/system/FeatureTogglesDto.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/model/system/FeatureTogglesDto.java index 29dd56269cf..ae19f9d2a40 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/model/system/FeatureTogglesDto.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/model/system/FeatureTogglesDto.java @@ -65,6 +65,9 @@ public class FeatureTogglesDto implements Serializable { @JsonProperty("modify_y_flow_enabled") private Boolean modifyYFlowEnabled; + @JsonProperty("sync_switch_on_connect") + private Boolean syncSwitchOnConnect; + @JsonCreator public FeatureTogglesDto(@JsonProperty("flows_reroute_on_isl_discovery") Boolean flowsRerouteOnIslDiscoveryEnabled, @JsonProperty("create_flow") Boolean createFlowEnabled, @@ -79,7 +82,8 @@ public FeatureTogglesDto(@JsonProperty("flows_reroute_on_isl_discovery") Boolean @JsonProperty("flow_latency_monitoring_reactions") Boolean flowLatencyMonitoringReactions, @JsonProperty("server42_isl_rtt") Boolean server42IslRtt, - @JsonProperty("modify_y_flow_enabled") Boolean modifyYFlowEnabled) { + @JsonProperty("modify_y_flow_enabled") Boolean modifyYFlowEnabled, + @JsonProperty("sync_switch_on_connect") Boolean syncSwitchOnConnect) { this.flowsRerouteOnIslDiscoveryEnabled = flowsRerouteOnIslDiscoveryEnabled; this.createFlowEnabled = createFlowEnabled; this.updateFlowEnabled = updateFlowEnabled; @@ -92,5 +96,6 @@ public FeatureTogglesDto(@JsonProperty("flows_reroute_on_isl_discovery") Boolean this.flowLatencyMonitoringReactions = flowLatencyMonitoringReactions; this.server42IslRtt = server42IslRtt; this.modifyYFlowEnabled = modifyYFlowEnabled; + this.syncSwitchOnConnect = syncSwitchOnConnect; } } diff --git a/src-java/kilda-model/src/main/java/org/openkilda/model/KildaFeatureToggles.java b/src-java/kilda-model/src/main/java/org/openkilda/model/KildaFeatureToggles.java index a0e95a23ce3..b9bf281bb7d 100644 --- a/src-java/kilda-model/src/main/java/org/openkilda/model/KildaFeatureToggles.java +++ b/src-java/kilda-model/src/main/java/org/openkilda/model/KildaFeatureToggles.java @@ -54,6 +54,7 @@ public class KildaFeatureToggles implements CompositeDataEntity invocation.getArgument(0)).when(switchRepository).add(any()); when(kildaConfigurationRepository.getOrDefault()).thenReturn(KildaConfiguration.DEFAULTS); + when(featureTogglesRepository.getOrDefault()).thenReturn(KildaFeatureToggles.DEFAULTS); reset(repositoryFactory); when(repositoryFactory.createSwitchRepository()).thenReturn(switchRepository); @@ -194,6 +200,7 @@ private void resetMocks() { when(repositoryFactory.createSwitchPropertiesRepository()).thenReturn(switchPropertiesRepository); when(repositoryFactory.createKildaConfigurationRepository()).thenReturn(kildaConfigurationRepository); when(repositoryFactory.createSpeakerRepository()).thenReturn(speakerRepository); + when(repositoryFactory.createFeatureTogglesRepository()).thenReturn(featureTogglesRepository); } @Test diff --git a/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/extension/env/EnvExtension.groovy b/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/extension/env/EnvExtension.groovy index 88709f691cd..6a7e3dab159 100644 --- a/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/extension/env/EnvExtension.groovy +++ b/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/extension/env/EnvExtension.groovy @@ -107,6 +107,7 @@ class EnvExtension extends AbstractGlobalExtension implements SpringContextListe .server42FlowRtt(true) .server42IslRtt(true) .modifyYFlowEnabled(true) + .syncSwitchOnConnect(true) .build() northbound.toggleFeature(features) log.info("Deleting all flows") diff --git a/src-java/testing/performance-tests/src/test/groovy/org/openkilda/performancetests/BaseSpecification.groovy b/src-java/testing/performance-tests/src/test/groovy/org/openkilda/performancetests/BaseSpecification.groovy index 8f3c1080d88..8a1f141c4ef 100644 --- a/src-java/testing/performance-tests/src/test/groovy/org/openkilda/performancetests/BaseSpecification.groovy +++ b/src-java/testing/performance-tests/src/test/groovy/org/openkilda/performancetests/BaseSpecification.groovy @@ -109,6 +109,7 @@ class BaseSpecification extends Specification { .deleteFlowEnabled(true) .flowsRerouteOnIslDiscoveryEnabled(true) .useBfdForIslIntegrityCheck(true) + .syncSwitchOnConnect(true) .build() northbound.toggleFeature(features) northbound.updateKildaConfiguration(KildaConfigurationDto.builder().useMultiTable(useMultitable).build()) From 9e3c077c52788a2f047ed624009ac802a1d44237 Mon Sep 17 00:00:00 2001 From: Dmitry Poltavets Date: Tue, 15 Feb 2022 15:59:27 +0400 Subject: [PATCH 03/11] Ignore leading and trailing spaces in a string for the SwitchId class. --- .../java/org/openkilda/model/SwitchId.java | 4 +-- .../org/openkilda/model/SwitchIdTest.java | 29 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src-java/kilda-model/src/main/java/org/openkilda/model/SwitchId.java b/src-java/kilda-model/src/main/java/org/openkilda/model/SwitchId.java index 17c3adee270..76f17192cda 100644 --- a/src-java/kilda-model/src/main/java/org/openkilda/model/SwitchId.java +++ b/src-java/kilda-model/src/main/java/org/openkilda/model/SwitchId.java @@ -1,4 +1,4 @@ -/* Copyright 2018 Telstra Open Source +/* Copyright 2022 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. @@ -46,7 +46,7 @@ public SwitchId(String switchId) { Objects.requireNonNull(switchId, "Switch id must not be null"); try { - this.id = Long.parseUnsignedLong(switchId.replaceAll("[-:]", ""), 16); + this.id = Long.parseUnsignedLong(switchId.replaceAll("[-:]", "").trim(), 16); } catch (NumberFormatException e) { throw new IllegalArgumentException(String.format("Can not parse input string: \"%s\"", switchId)); } diff --git a/src-java/kilda-model/src/test/java/org/openkilda/model/SwitchIdTest.java b/src-java/kilda-model/src/test/java/org/openkilda/model/SwitchIdTest.java index a32f8cc47bb..8f7701cdabe 100644 --- a/src-java/kilda-model/src/test/java/org/openkilda/model/SwitchIdTest.java +++ b/src-java/kilda-model/src/test/java/org/openkilda/model/SwitchIdTest.java @@ -1,4 +1,4 @@ -/* Copyright 2018 Telstra Open Source +/* Copyright 2022 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. @@ -15,7 +15,8 @@ package org.openkilda.model; -import org.junit.Assert; +import static org.junit.jupiter.api.Assertions.assertEquals; + import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -59,13 +60,21 @@ public void colonSeparatedBytesPositive() { char[] hexArray = String.format("%016x", switchId.toLong()).toCharArray(); - Assert.assertEquals(switchIdString, switchId.colonSeparatedBytes(hexArray, 0)); - Assert.assertEquals(switchIdString.substring(3), switchId.colonSeparatedBytes(hexArray, 2)); - Assert.assertEquals(switchIdString.substring(6), switchId.colonSeparatedBytes(hexArray, 4)); - Assert.assertEquals(switchIdString.substring(9), switchId.colonSeparatedBytes(hexArray, 6)); - Assert.assertEquals(switchIdString.substring(12), switchId.colonSeparatedBytes(hexArray, 8)); - Assert.assertEquals(switchIdString.substring(15), switchId.colonSeparatedBytes(hexArray, 10)); - Assert.assertEquals(switchIdString.substring(18), switchId.colonSeparatedBytes(hexArray, 12)); - Assert.assertEquals(switchIdString.substring(21), switchId.colonSeparatedBytes(hexArray, 14)); + assertEquals(switchIdString, switchId.colonSeparatedBytes(hexArray, 0)); + assertEquals(switchIdString.substring(3), switchId.colonSeparatedBytes(hexArray, 2)); + assertEquals(switchIdString.substring(6), switchId.colonSeparatedBytes(hexArray, 4)); + assertEquals(switchIdString.substring(9), switchId.colonSeparatedBytes(hexArray, 6)); + assertEquals(switchIdString.substring(12), switchId.colonSeparatedBytes(hexArray, 8)); + assertEquals(switchIdString.substring(15), switchId.colonSeparatedBytes(hexArray, 10)); + assertEquals(switchIdString.substring(18), switchId.colonSeparatedBytes(hexArray, 12)); + assertEquals(switchIdString.substring(21), switchId.colonSeparatedBytes(hexArray, 14)); + } + + @Test + public void trimStringForSwitchId() { + String switchIdString = " fe:dc:ba:98:76:54:32:10 "; + SwitchId switchId = new SwitchId(switchIdString); + + assertEquals(new SwitchId("fe:dc:ba:98:76:54:32:10"), switchId); } } From ddb9d2f60ef7fa86bb3183056ecd441e72014250 Mon Sep 17 00:00:00 2001 From: Sergii Iakovenko Date: Wed, 16 Feb 2022 16:30:53 +0200 Subject: [PATCH 04/11] Fix excess y-flow meters (proper building & handing of DeleteSpeakerCommandsRequest) --- .../command/rulemanager/BatchData.java | 1 + .../command/rulemanager/OfBatchExecutor.java | 80 +++++++++++------ .../command/rulemanager/OfBatchHolder.java | 27 ++++-- .../rulemanager/OfFlowConverter.java | 1 + .../BaseSpeakerResponseProcessingAction.java | 87 +++++++++++++++++++ .../YFlowRuleManagerProcessingAction.java | 32 ++++++- .../OnReceivedInstallResponseAction.java | 8 +- .../OnReceivedRemoveResponseAction.java | 8 +- .../OnReceivedRemoveResponseAction.java | 8 +- .../OnReceivedInstallResponseAction.java | 8 +- .../OnReceivedRemoveResponseAction.java | 8 +- .../OnReceivedInstallResponseAction.java | 8 +- .../OnReceivedRemoveResponseAction.java | 8 +- .../rulemanager/FlowSpeakerData.java | 2 +- .../rulemanager/GroupSpeakerData.java | 2 +- .../rulemanager/MeterSpeakerData.java | 4 +- .../openkilda/rulemanager/SpeakerData.java | 2 +- 17 files changed, 231 insertions(+), 63 deletions(-) create mode 100644 src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/common/actions/BaseSpeakerResponseProcessingAction.java diff --git a/src-java/floodlight-service/floodlight-modules/src/main/java/org/openkilda/floodlight/command/rulemanager/BatchData.java b/src-java/floodlight-service/floodlight-modules/src/main/java/org/openkilda/floodlight/command/rulemanager/BatchData.java index 46bc14ea7e9..ac3c830e13d 100644 --- a/src-java/floodlight-service/floodlight-modules/src/main/java/org/openkilda/floodlight/command/rulemanager/BatchData.java +++ b/src-java/floodlight-service/floodlight-modules/src/main/java/org/openkilda/floodlight/command/rulemanager/BatchData.java @@ -26,4 +26,5 @@ public class BatchData { private boolean group; private boolean flow; private OFMessage message; + private boolean presenceBeVerified; } diff --git a/src-java/floodlight-service/floodlight-modules/src/main/java/org/openkilda/floodlight/command/rulemanager/OfBatchExecutor.java b/src-java/floodlight-service/floodlight-modules/src/main/java/org/openkilda/floodlight/command/rulemanager/OfBatchExecutor.java index ae578f856ea..428e0b16133 100644 --- a/src-java/floodlight-service/floodlight-modules/src/main/java/org/openkilda/floodlight/command/rulemanager/OfBatchExecutor.java +++ b/src-java/floodlight-service/floodlight-modules/src/main/java/org/openkilda/floodlight/command/rulemanager/OfBatchExecutor.java @@ -116,15 +116,21 @@ public void executeBatch() { try (Session session = sessionService.open(messageContext, iofSwitch)) { for (OFMessage message : ofMessages) { requests.add(session.write(message).whenComplete((res, ex) -> { - log.debug("Check responses (key={})", kafkaKey); + log.debug("Check responses (key={}, xid={}, res={}, ex={})", kafkaKey, message.getXid(), res, ex); if (ex == null) { res.ifPresent(ofMessage -> { - UUID uuid = holder.popAwaitingXid(ofMessage.getXid()); if (ofMessage instanceof OFErrorMsg) { + UUID uuid = holder.popAwaitingXid(ofMessage.getXid()); OFErrorMsg errorMsg = (OFErrorMsg) ofMessage; holder.recordFailedUuid(uuid, errorMsg.getErrType().toString()); + } else { + onSuccessfulOfMessage(ofMessage); } }); + // session.write() completes successfully with no result. + if (!res.isPresent()) { + onSuccessfulOfMessage(message); + } } else { log.error("Received error {}", ex.getMessage(), ex); } @@ -136,6 +142,17 @@ public void executeBatch() { .thenAccept(ignore -> checkOfResponses()); } + private void onSuccessfulOfMessage(OFMessage ofMessage) { + UUID uuid = holder.popAwaitingXid(ofMessage.getXid()); + BatchData batchData = holder.getByUUid(uuid); + if (batchData != null && !batchData.isPresenceBeVerified()) { + holder.recordSuccessUuid(uuid); + } else { + log.debug("Received a response for {} / {}. Batch is to be verified...", + ofMessage.getXid(), uuid); + } + } + private void checkOfResponses() { if (hasMeters) { meterStats = OfUtils.verifyMeters(messageContext, iofSwitch).whenComplete((res, ex) -> { @@ -172,7 +189,6 @@ private void runVerify() { verifyGroups(); if (holder.nextStage()) { log.debug("Proceed next stage (key={})", kafkaKey); - holder.resetXids(); meterStats = CompletableFuture.completedFuture(null); groupStats = CompletableFuture.completedFuture(null); flowStats = CompletableFuture.completedFuture(null); @@ -183,7 +199,6 @@ private void runVerify() { } else { sendResponse(); } - } private void verifyFlows() { @@ -204,19 +219,22 @@ private void verifyFlows() { for (FlowSpeakerData switchFlow : switchFlows) { FlowSpeakerData expectedFlow = holder.getByCookie(switchFlow.getCookie()); if (expectedFlow != null) { - if (switchFlow.equals(expectedFlow)) { - holder.recordSuccessUuid(expectedFlow.getUuid()); - } else { - long cookie = switchFlow.getCookie().getValue(); - // Go through all duplicate cookies, and fail on the last one. - if (cookieCounts.get(cookie) > 1) { - log.debug("Detected duplicate cookies {} on switch {}, skipping...", switchFlow.getCookie(), - switchFlow.getSwitchId()); - cookieCounts.compute(cookie, (k, v) -> v - 1); + BatchData batchData = holder.getByUUid(expectedFlow.getUuid()); + if (batchData != null && batchData.isPresenceBeVerified()) { + if (switchFlow.equals(expectedFlow)) { + holder.recordSuccessUuid(expectedFlow.getUuid()); } else { - holder.recordFailedUuid(expectedFlow.getUuid(), - format("Failed to validate flow on a switch. Expected: %s, actual: %s", - expectedFlow, switchFlow)); + long cookie = switchFlow.getCookie().getValue(); + // Go through all duplicate cookies, and fail on the last one. + if (cookieCounts.get(cookie) > 1) { + log.debug("Detected duplicate cookies {} on switch {}, skipping...", + switchFlow.getCookie(), switchFlow.getSwitchId()); + cookieCounts.compute(cookie, (k, v) -> v - 1); + } else { + holder.recordFailedUuid(expectedFlow.getUuid(), + format("Failed to validate flow on a switch. Expected: %s, actual: %s", + expectedFlow, switchFlow)); + } } } } @@ -241,12 +259,15 @@ private void verifyMeters() { for (MeterSpeakerData switchMeter : switchMeters) { MeterSpeakerData expectedMeter = holder.getByMeterId(switchMeter.getMeterId()); if (expectedMeter != null) { - if (switchMeter.equals(expectedMeter)) { - holder.recordSuccessUuid(expectedMeter.getUuid()); - } else { - holder.recordFailedUuid(expectedMeter.getUuid(), - format("Failed to validate meter on a switch. Expected: %s, actual: %s. " - + "Switch features: %s.", expectedMeter, switchMeter, switchFeatures)); + BatchData batchData = holder.getByUUid(expectedMeter.getUuid()); + if (batchData != null && batchData.isPresenceBeVerified()) { + if (switchMeter.equals(expectedMeter)) { + holder.recordSuccessUuid(expectedMeter.getUuid()); + } else { + holder.recordFailedUuid(expectedMeter.getUuid(), + format("Failed to validate meter on a switch. Expected: %s, actual: %s. " + + "Switch features: %s.", expectedMeter, switchMeter, switchFeatures)); + } } } } @@ -270,12 +291,15 @@ private void verifyGroups() { for (GroupSpeakerData switchGroup : switchGroups) { GroupSpeakerData expectedGroup = holder.getByGroupId(switchGroup.getGroupId()); if (expectedGroup != null) { - if (switchGroup.equals(expectedGroup)) { - holder.recordSuccessUuid(expectedGroup.getUuid()); - } else { - holder.recordFailedUuid(expectedGroup.getUuid(), - format("Failed to validate group on a switch. Expected: %s, actual: %s", expectedGroup, - switchGroup)); + BatchData batchData = holder.getByUUid(expectedGroup.getUuid()); + if (batchData != null && batchData.isPresenceBeVerified()) { + if (switchGroup.equals(expectedGroup)) { + holder.recordSuccessUuid(expectedGroup.getUuid()); + } else { + holder.recordFailedUuid(expectedGroup.getUuid(), + format("Failed to validate group on a switch. Expected: %s, actual: %s", + expectedGroup, switchGroup)); + } } } } diff --git a/src-java/floodlight-service/floodlight-modules/src/main/java/org/openkilda/floodlight/command/rulemanager/OfBatchHolder.java b/src-java/floodlight-service/floodlight-modules/src/main/java/org/openkilda/floodlight/command/rulemanager/OfBatchHolder.java index 21cedfe8a97..7c6eaecdfcc 100644 --- a/src-java/floodlight-service/floodlight-modules/src/main/java/org/openkilda/floodlight/command/rulemanager/OfBatchHolder.java +++ b/src-java/floodlight-service/floodlight-modules/src/main/java/org/openkilda/floodlight/command/rulemanager/OfBatchHolder.java @@ -77,9 +77,9 @@ public void resetXids() { xidMapping = new HashMap<>(); } - public void recordSuccessUuid(UUID failedUuid) { - log.debug("Record success for {}", failedUuid); - successUuids.add(failedUuid); + public void recordSuccessUuid(UUID successUuid) { + log.debug("Record success for {}", successUuid); + successUuids.add(successUuid); } /** @@ -119,6 +119,8 @@ public Map getBlockingDependencies(UUID uuid) { if (!successUuids.contains(dep)) { if (failedUuids.containsKey(dep)) { result.put(dep, failedUuids.get(dep)); + } else if (!commandMap.containsKey(dep)) { + result.put(dep, "Missing in the batch"); } else { result.put(dep, "Not executed yet"); } @@ -144,6 +146,7 @@ public GroupSpeakerData getByGroupId(GroupId groupId) { } public UUID popAwaitingXid(long xid) { + log.trace("popAwaitingXid for {}, current uuid: {}", xid, xidMapping.get(xid)); return xidMapping.remove(xid); } @@ -161,7 +164,8 @@ public void addInstallFlow(FlowSpeakerData data, SwitchId switchId) { OFFactory factory = iofSwitchService.getSwitch(dpId).getOFFactory(); OFMessage message = OfFlowConverter.INSTANCE.convertInstallFlowCommand(data, factory); xidMapping.put(message.getXid(), data.getUuid()); - commandMap.put(data.getUuid(), BatchData.builder().flow(true).message(message).build()); + BatchData batchData = BatchData.builder().flow(true).message(message).presenceBeVerified(true).build(); + commandMap.put(data.getUuid(), batchData); flowsMap.put(data.getCookie(), data); executionGraph.add(data.getUuid(), data.getDependsOn()); } @@ -172,7 +176,8 @@ public void addDeleteFlow(FlowSpeakerData data, SwitchId switchId) { OFFactory factory = iofSwitchService.getSwitch(dpId).getOFFactory(); OFMessage message = OfFlowConverter.INSTANCE.convertDeleteFlowCommand(data, factory); xidMapping.put(message.getXid(), data.getUuid()); - commandMap.put(data.getUuid(), BatchData.builder().flow(true).message(message).build()); + BatchData batchData = BatchData.builder().flow(true).message(message).presenceBeVerified(false).build(); + commandMap.put(data.getUuid(), batchData); flowsMap.put(data.getCookie(), data); executionGraph.add(data.getUuid(), data.getDependsOn()); } @@ -183,7 +188,8 @@ public void addInstallMeter(MeterSpeakerData data, SwitchId switchId) { OFFactory factory = iofSwitchService.getSwitch(dpId).getOFFactory(); OFMessage message = OfMeterConverter.INSTANCE.convertInstallMeterCommand(data, factory); xidMapping.put(message.getXid(), data.getUuid()); - commandMap.put(data.getUuid(), BatchData.builder().meter(true).message(message).build()); + BatchData batchData = BatchData.builder().meter(true).message(message).presenceBeVerified(true).build(); + commandMap.put(data.getUuid(), batchData); metersMap.put(data.getMeterId(), data); executionGraph.add(data.getUuid(), data.getDependsOn()); } @@ -194,7 +200,8 @@ public void addDeleteMeter(MeterSpeakerData data, SwitchId switchId) { OFFactory factory = iofSwitchService.getSwitch(dpId).getOFFactory(); OFMessage message = OfMeterConverter.INSTANCE.convertDeleteMeterCommand(data, factory); xidMapping.put(message.getXid(), data.getUuid()); - commandMap.put(data.getUuid(), BatchData.builder().meter(true).message(message).build()); + BatchData batchData = BatchData.builder().meter(true).message(message).presenceBeVerified(false).build(); + commandMap.put(data.getUuid(), batchData); metersMap.put(data.getMeterId(), data); executionGraph.add(data.getUuid(), data.getDependsOn()); } @@ -205,7 +212,8 @@ public void addInstallGroup(GroupSpeakerData data, SwitchId switchId) { OFFactory factory = iofSwitchService.getSwitch(dpId).getOFFactory(); OFMessage message = OfGroupConverter.INSTANCE.convertInstallGroupCommand(data, factory); xidMapping.put(message.getXid(), data.getUuid()); - commandMap.put(data.getUuid(), BatchData.builder().group(true).message(message).build()); + BatchData batchData = BatchData.builder().group(true).message(message).presenceBeVerified(true).build(); + commandMap.put(data.getUuid(), batchData); groupsMap.put(data.getGroupId(), data); executionGraph.add(data.getUuid(), data.getDependsOn()); } @@ -216,7 +224,8 @@ public void addDeleteGroup(GroupSpeakerData data, SwitchId switchId) { OFFactory factory = iofSwitchService.getSwitch(dpId).getOFFactory(); OFMessage message = OfGroupConverter.INSTANCE.convertDeleteGroupCommand(data, factory); xidMapping.put(message.getXid(), data.getUuid()); - commandMap.put(data.getUuid(), BatchData.builder().group(true).message(message).build()); + BatchData batchData = BatchData.builder().group(true).message(message).presenceBeVerified(false).build(); + commandMap.put(data.getUuid(), batchData); groupsMap.put(data.getGroupId(), data); executionGraph.add(data.getUuid(), data.getDependsOn()); } diff --git a/src-java/floodlight-service/floodlight-modules/src/main/java/org/openkilda/floodlight/converter/rulemanager/OfFlowConverter.java b/src-java/floodlight-service/floodlight-modules/src/main/java/org/openkilda/floodlight/converter/rulemanager/OfFlowConverter.java index 5e0510f55e7..975780542e5 100644 --- a/src-java/floodlight-service/floodlight-modules/src/main/java/org/openkilda/floodlight/converter/rulemanager/OfFlowConverter.java +++ b/src-java/floodlight-service/floodlight-modules/src/main/java/org/openkilda/floodlight/converter/rulemanager/OfFlowConverter.java @@ -102,6 +102,7 @@ public OFFlowMod convertInstallFlowCommand(FlowSpeakerData commandData, OFFactor public OFFlowMod convertDeleteFlowCommand(FlowSpeakerData commandData, OFFactory ofFactory) { return ofFactory.buildFlowDeleteStrict() .setCookie(U64.of(commandData.getCookie().getValue())) + .setCookieMask(U64.NO_MASK) .setTableId(TableId.of(commandData.getTable().getTableId())) .setPriority(commandData.getPriority()) .setMatch(OfMatchConverter.INSTANCE.convertMatch(commandData.getMatch(), ofFactory)) diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/common/actions/BaseSpeakerResponseProcessingAction.java b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/common/actions/BaseSpeakerResponseProcessingAction.java new file mode 100644 index 00000000000..226ce71c45c --- /dev/null +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/common/actions/BaseSpeakerResponseProcessingAction.java @@ -0,0 +1,87 @@ +/* Copyright 2022 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.fsm.common.actions; + +import static java.util.stream.Collectors.toList; + +import org.openkilda.floodlight.api.request.rulemanager.FlowCommand; +import org.openkilda.floodlight.api.request.rulemanager.GroupCommand; +import org.openkilda.floodlight.api.request.rulemanager.MeterCommand; +import org.openkilda.floodlight.api.request.rulemanager.OfCommand; +import org.openkilda.rulemanager.FlowSpeakerData; +import org.openkilda.rulemanager.GroupSpeakerData; +import org.openkilda.rulemanager.MeterSpeakerData; +import org.openkilda.wfm.topology.flowhs.fsm.common.FlowProcessingWithHistorySupportFsm; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +public abstract class BaseSpeakerResponseProcessingAction, S, E, C> extends HistoryRecordingAction { + + protected List removeExcessDependencies(List source) { + Set allUuids = source.stream().map(command -> { + if (command instanceof FlowCommand) { + return ((FlowCommand) command).getData().getUuid(); + } else if (command instanceof MeterCommand) { + return ((MeterCommand) command).getData().getUuid(); + } else if (command instanceof GroupCommand) { + return ((GroupCommand) command).getData().getUuid(); + } else { + throw new IllegalArgumentException("Unknown speaker command type: " + command); + } + }).collect(Collectors.toSet()); + return source.stream() + .map(command -> recreateCommandWithDependedncies(command, allUuids)) + .collect(toList()); + } + + private OfCommand recreateCommandWithDependedncies(OfCommand command, Set allUuids) { + if (command instanceof FlowCommand) { + FlowSpeakerData data = ((FlowCommand) command).getData(); + if (allUuids.containsAll(data.getDependsOn())) { + return command; + } else { + Set resultDependsOn = new HashSet<>(data.getDependsOn()); + resultDependsOn.retainAll(allUuids); + return new FlowCommand(data.toBuilder().dependsOn(resultDependsOn).build()); + } + } else if (command instanceof MeterCommand) { + MeterSpeakerData data = ((MeterCommand) command).getData(); + if (allUuids.containsAll(data.getDependsOn())) { + return command; + } else { + Set resultDependsOn = new HashSet<>(data.getDependsOn()); + resultDependsOn.retainAll(allUuids); + return new MeterCommand(data.toBuilder().dependsOn(resultDependsOn).build()); + } + } else if (command instanceof GroupCommand) { + GroupSpeakerData data = ((GroupCommand) command).getData(); + if (allUuids.containsAll(data.getDependsOn())) { + return command; + } else { + Set resultDependsOn = new HashSet<>(data.getDependsOn()); + resultDependsOn.retainAll(allUuids); + return new GroupCommand(data.toBuilder().dependsOn(resultDependsOn).build()); + } + } else { + throw new IllegalArgumentException("Unknown speaker command type: " + command); + } + } +} diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/common/actions/YFlowRuleManagerProcessingAction.java b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/common/actions/YFlowRuleManagerProcessingAction.java index d28e6d2a31e..e06b2b7abc7 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/common/actions/YFlowRuleManagerProcessingAction.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/common/actions/YFlowRuleManagerProcessingAction.java @@ -15,6 +15,7 @@ package org.openkilda.wfm.topology.flowhs.fsm.common.actions; +import static java.util.Collections.emptySet; import static java.util.stream.Collectors.toList; import org.openkilda.floodlight.api.request.rulemanager.DeleteSpeakerCommandsRequest; @@ -45,6 +46,8 @@ import lombok.extern.slf4j.Slf4j; import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -80,13 +83,40 @@ protected Collection buildYFlowDeleteCommands(YFlo return speakerData.entrySet().stream().map(entry -> { SwitchId switchId = entry.getKey(); - List dataList = entry.getValue(); + List dataList = reverseDependenciesForDeletion(entry.getValue()); UUID commandId = commandIdGenerator.generate(); MessageContext messageContext = new MessageContext(commandId.toString(), context.getCorrelationId()); return new DeleteSpeakerCommandsRequest(messageContext, switchId, commandId, mapToOfCommands(dataList)); }).collect(Collectors.toList()); } + private List reverseDependenciesForDeletion(List source) { + Map> reversedDependencies = new HashMap<>(); + source.forEach(data -> + data.getDependsOn().forEach(dependent -> + reversedDependencies.computeIfAbsent(dependent, k -> new HashSet<>()).add(data.getUuid()))); + return source.stream() + .map(data -> { + Set reversedDependsOn = reversedDependencies.getOrDefault(data.getUuid(), emptySet()); + if (data instanceof FlowSpeakerData) { + return ((FlowSpeakerData) data).toBuilder() + .dependsOn(reversedDependsOn) + .build(); + } else if (data instanceof MeterSpeakerData) { + return ((MeterSpeakerData) data).toBuilder() + .dependsOn(reversedDependsOn) + .build(); + } else if (data instanceof GroupSpeakerData) { + return ((GroupSpeakerData) data).toBuilder() + .dependsOn(reversedDependsOn) + .build(); + } else { + throw new IllegalArgumentException("Unknown speaker data type: " + data); + } + }) + .collect(toList()); + } + private Map> buildYFlowSpeakerData(YFlow yFlow) { List flowPaths = yFlow.getSubFlows().stream() .map(YSubFlow::getFlow) diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/create/actions/OnReceivedInstallResponseAction.java b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/create/actions/OnReceivedInstallResponseAction.java index 423e230fba8..96cd2e6282a 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/create/actions/OnReceivedInstallResponseAction.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/create/actions/OnReceivedInstallResponseAction.java @@ -23,7 +23,7 @@ import org.openkilda.floodlight.api.request.rulemanager.MeterCommand; import org.openkilda.floodlight.api.request.rulemanager.OfCommand; import org.openkilda.floodlight.api.response.rulemanager.SpeakerCommandResponse; -import org.openkilda.wfm.topology.flowhs.fsm.common.actions.HistoryRecordingAction; +import org.openkilda.wfm.topology.flowhs.fsm.common.actions.BaseSpeakerResponseProcessingAction; import org.openkilda.wfm.topology.flowhs.fsm.yflow.create.YFlowCreateContext; import org.openkilda.wfm.topology.flowhs.fsm.yflow.create.YFlowCreateFsm; import org.openkilda.wfm.topology.flowhs.fsm.yflow.create.YFlowCreateFsm.Event; @@ -39,7 +39,7 @@ @Slf4j public class OnReceivedInstallResponseAction extends - HistoryRecordingAction { + BaseSpeakerResponseProcessingAction { private static final String FAILED_TO_INSTALL_RULE_ACTION = "Failed to install rule"; private final int speakerCommandRetriesLimit; @@ -81,7 +81,9 @@ public void perform(State from, State to, Event event, YFlowCreateContext contex || command instanceof GroupCommand && failedUuids.contains(((GroupCommand) command).getData().getUuid())) .collect(Collectors.toList()); - stateMachine.getCarrier().sendSpeakerRequest(installRequest.toBuilder().commands(commands).build()); + InstallSpeakerCommandsRequest retryRequest = installRequest.toBuilder() + .commands(removeExcessDependencies(commands)).build(); + stateMachine.getCarrier().sendSpeakerRequest(retryRequest); } else { stateMachine.addFailedCommand(commandId, response); stateMachine.removePendingCommand(commandId); diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/create/actions/OnReceivedRemoveResponseAction.java b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/create/actions/OnReceivedRemoveResponseAction.java index db0a0d40fa3..27500cee3a9 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/create/actions/OnReceivedRemoveResponseAction.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/create/actions/OnReceivedRemoveResponseAction.java @@ -23,7 +23,7 @@ import org.openkilda.floodlight.api.request.rulemanager.MeterCommand; import org.openkilda.floodlight.api.request.rulemanager.OfCommand; import org.openkilda.floodlight.api.response.rulemanager.SpeakerCommandResponse; -import org.openkilda.wfm.topology.flowhs.fsm.common.actions.HistoryRecordingAction; +import org.openkilda.wfm.topology.flowhs.fsm.common.actions.BaseSpeakerResponseProcessingAction; import org.openkilda.wfm.topology.flowhs.fsm.yflow.create.YFlowCreateContext; import org.openkilda.wfm.topology.flowhs.fsm.yflow.create.YFlowCreateFsm; import org.openkilda.wfm.topology.flowhs.fsm.yflow.create.YFlowCreateFsm.Event; @@ -39,7 +39,7 @@ @Slf4j public class OnReceivedRemoveResponseAction extends - HistoryRecordingAction { + BaseSpeakerResponseProcessingAction { private static final String FAILED_TO_REMOVE_RULE_ACTION = "Failed to remove rule"; private final int speakerCommandRetriesLimit; @@ -81,7 +81,9 @@ public void perform(State from, State to, Event event, YFlowCreateContext contex || command instanceof GroupCommand && failedUuids.contains(((GroupCommand) command).getData().getUuid())) .collect(Collectors.toList()); - stateMachine.getCarrier().sendSpeakerRequest(deleteRequest.toBuilder().commands(commands).build()); + DeleteSpeakerCommandsRequest retryRequest = deleteRequest.toBuilder() + .commands(removeExcessDependencies(commands)).build(); + stateMachine.getCarrier().sendSpeakerRequest(retryRequest); } else { stateMachine.addFailedCommand(commandId, response); stateMachine.removePendingCommand(commandId); diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/delete/actions/OnReceivedRemoveResponseAction.java b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/delete/actions/OnReceivedRemoveResponseAction.java index d4c9e7e2cf7..91916639ee9 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/delete/actions/OnReceivedRemoveResponseAction.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/delete/actions/OnReceivedRemoveResponseAction.java @@ -23,7 +23,7 @@ import org.openkilda.floodlight.api.request.rulemanager.MeterCommand; import org.openkilda.floodlight.api.request.rulemanager.OfCommand; import org.openkilda.floodlight.api.response.rulemanager.SpeakerCommandResponse; -import org.openkilda.wfm.topology.flowhs.fsm.common.actions.HistoryRecordingAction; +import org.openkilda.wfm.topology.flowhs.fsm.common.actions.BaseSpeakerResponseProcessingAction; import org.openkilda.wfm.topology.flowhs.fsm.yflow.delete.YFlowDeleteContext; import org.openkilda.wfm.topology.flowhs.fsm.yflow.delete.YFlowDeleteFsm; import org.openkilda.wfm.topology.flowhs.fsm.yflow.delete.YFlowDeleteFsm.Event; @@ -39,7 +39,7 @@ @Slf4j public class OnReceivedRemoveResponseAction extends - HistoryRecordingAction { + BaseSpeakerResponseProcessingAction { private static final String FAILED_TO_REMOVE_RULE_ACTION = "Failed to remove rule"; private final int speakerCommandRetriesLimit; @@ -81,7 +81,9 @@ public void perform(State from, State to, Event event, YFlowDeleteContext contex || command instanceof GroupCommand && failedUuids.contains(((GroupCommand) command).getData().getUuid())) .collect(Collectors.toList()); - stateMachine.getCarrier().sendSpeakerRequest(deleteRequest.toBuilder().commands(commands).build()); + DeleteSpeakerCommandsRequest retryRequest = deleteRequest.toBuilder() + .commands(removeExcessDependencies(commands)).build(); + stateMachine.getCarrier().sendSpeakerRequest(retryRequest); } else { stateMachine.addFailedCommand(commandId, response); stateMachine.removePendingCommand(commandId); diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/reroute/actions/OnReceivedInstallResponseAction.java b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/reroute/actions/OnReceivedInstallResponseAction.java index a195d24465f..3ee8adb56b1 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/reroute/actions/OnReceivedInstallResponseAction.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/reroute/actions/OnReceivedInstallResponseAction.java @@ -23,7 +23,7 @@ import org.openkilda.floodlight.api.request.rulemanager.MeterCommand; import org.openkilda.floodlight.api.request.rulemanager.OfCommand; import org.openkilda.floodlight.api.response.rulemanager.SpeakerCommandResponse; -import org.openkilda.wfm.topology.flowhs.fsm.common.actions.HistoryRecordingAction; +import org.openkilda.wfm.topology.flowhs.fsm.common.actions.BaseSpeakerResponseProcessingAction; import org.openkilda.wfm.topology.flowhs.fsm.yflow.reroute.YFlowRerouteContext; import org.openkilda.wfm.topology.flowhs.fsm.yflow.reroute.YFlowRerouteFsm; import org.openkilda.wfm.topology.flowhs.fsm.yflow.reroute.YFlowRerouteFsm.Event; @@ -39,7 +39,7 @@ @Slf4j public class OnReceivedInstallResponseAction extends - HistoryRecordingAction { + BaseSpeakerResponseProcessingAction { private static final String FAILED_TO_INSTALL_RULE_ACTION = "Failed to install rule"; private final int speakerCommandRetriesLimit; @@ -81,7 +81,9 @@ public void perform(State from, State to, Event event, YFlowRerouteContext conte || command instanceof GroupCommand && failedUuids.contains(((GroupCommand) command).getData().getUuid())) .collect(Collectors.toList()); - stateMachine.getCarrier().sendSpeakerRequest(installRequest.toBuilder().commands(commands).build()); + InstallSpeakerCommandsRequest retryRequest = installRequest.toBuilder() + .commands(removeExcessDependencies(commands)).build(); + stateMachine.getCarrier().sendSpeakerRequest(retryRequest); } else { stateMachine.addFailedCommand(commandId, response); stateMachine.removePendingCommand(commandId); diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/reroute/actions/OnReceivedRemoveResponseAction.java b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/reroute/actions/OnReceivedRemoveResponseAction.java index 9126d87fbc3..3c21c5a46d7 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/reroute/actions/OnReceivedRemoveResponseAction.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/reroute/actions/OnReceivedRemoveResponseAction.java @@ -23,7 +23,7 @@ import org.openkilda.floodlight.api.request.rulemanager.MeterCommand; import org.openkilda.floodlight.api.request.rulemanager.OfCommand; import org.openkilda.floodlight.api.response.rulemanager.SpeakerCommandResponse; -import org.openkilda.wfm.topology.flowhs.fsm.common.actions.HistoryRecordingAction; +import org.openkilda.wfm.topology.flowhs.fsm.common.actions.BaseSpeakerResponseProcessingAction; import org.openkilda.wfm.topology.flowhs.fsm.yflow.reroute.YFlowRerouteContext; import org.openkilda.wfm.topology.flowhs.fsm.yflow.reroute.YFlowRerouteFsm; import org.openkilda.wfm.topology.flowhs.fsm.yflow.reroute.YFlowRerouteFsm.Event; @@ -39,7 +39,7 @@ @Slf4j public class OnReceivedRemoveResponseAction extends - HistoryRecordingAction { + BaseSpeakerResponseProcessingAction { private static final String FAILED_TO_REMOVE_RULE_ACTION = "Failed to remove rule"; private final int speakerCommandRetriesLimit; @@ -81,7 +81,9 @@ public void perform(State from, State to, Event event, YFlowRerouteContext conte || command instanceof GroupCommand && failedUuids.contains(((GroupCommand) command).getData().getUuid())) .collect(Collectors.toList()); - stateMachine.getCarrier().sendSpeakerRequest(deleteRequest.toBuilder().commands(commands).build()); + DeleteSpeakerCommandsRequest retryRequest = deleteRequest.toBuilder() + .commands(removeExcessDependencies(commands)).build(); + stateMachine.getCarrier().sendSpeakerRequest(retryRequest); } else { stateMachine.addFailedCommand(commandId, response); stateMachine.removePendingCommand(commandId); diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/update/actions/OnReceivedInstallResponseAction.java b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/update/actions/OnReceivedInstallResponseAction.java index edb3da1fbca..f44b0979467 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/update/actions/OnReceivedInstallResponseAction.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/update/actions/OnReceivedInstallResponseAction.java @@ -23,7 +23,7 @@ import org.openkilda.floodlight.api.request.rulemanager.MeterCommand; import org.openkilda.floodlight.api.request.rulemanager.OfCommand; import org.openkilda.floodlight.api.response.rulemanager.SpeakerCommandResponse; -import org.openkilda.wfm.topology.flowhs.fsm.common.actions.HistoryRecordingAction; +import org.openkilda.wfm.topology.flowhs.fsm.common.actions.BaseSpeakerResponseProcessingAction; import org.openkilda.wfm.topology.flowhs.fsm.yflow.update.YFlowUpdateContext; import org.openkilda.wfm.topology.flowhs.fsm.yflow.update.YFlowUpdateFsm; import org.openkilda.wfm.topology.flowhs.fsm.yflow.update.YFlowUpdateFsm.Event; @@ -39,7 +39,7 @@ @Slf4j public class OnReceivedInstallResponseAction extends - HistoryRecordingAction { + BaseSpeakerResponseProcessingAction { private static final String FAILED_TO_INSTALL_RULE_ACTION = "Failed to install rule"; private final int speakerCommandRetriesLimit; @@ -81,7 +81,9 @@ public void perform(State from, State to, Event event, YFlowUpdateContext contex || command instanceof GroupCommand && failedUuids.contains(((GroupCommand) command).getData().getUuid())) .collect(Collectors.toList()); - stateMachine.getCarrier().sendSpeakerRequest(installRequest.toBuilder().commands(commands).build()); + InstallSpeakerCommandsRequest retryRequest = installRequest.toBuilder() + .commands(removeExcessDependencies(commands)).build(); + stateMachine.getCarrier().sendSpeakerRequest(retryRequest); } else { stateMachine.addFailedCommand(commandId, response); stateMachine.removePendingCommand(commandId); diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/update/actions/OnReceivedRemoveResponseAction.java b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/update/actions/OnReceivedRemoveResponseAction.java index 25a57ef78cb..b78cf87d28d 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/update/actions/OnReceivedRemoveResponseAction.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/yflow/update/actions/OnReceivedRemoveResponseAction.java @@ -23,7 +23,7 @@ import org.openkilda.floodlight.api.request.rulemanager.MeterCommand; import org.openkilda.floodlight.api.request.rulemanager.OfCommand; import org.openkilda.floodlight.api.response.rulemanager.SpeakerCommandResponse; -import org.openkilda.wfm.topology.flowhs.fsm.common.actions.HistoryRecordingAction; +import org.openkilda.wfm.topology.flowhs.fsm.common.actions.BaseSpeakerResponseProcessingAction; import org.openkilda.wfm.topology.flowhs.fsm.yflow.update.YFlowUpdateContext; import org.openkilda.wfm.topology.flowhs.fsm.yflow.update.YFlowUpdateFsm; import org.openkilda.wfm.topology.flowhs.fsm.yflow.update.YFlowUpdateFsm.Event; @@ -39,7 +39,7 @@ @Slf4j public class OnReceivedRemoveResponseAction extends - HistoryRecordingAction { + BaseSpeakerResponseProcessingAction { private static final String FAILED_TO_REMOVE_RULE_ACTION = "Failed to remove rule"; private final int speakerCommandRetriesLimit; @@ -81,7 +81,9 @@ public void perform(State from, State to, Event event, YFlowUpdateContext contex || command instanceof GroupCommand && failedUuids.contains(((GroupCommand) command).getData().getUuid())) .collect(Collectors.toList()); - stateMachine.getCarrier().sendSpeakerRequest(deleteRequest.toBuilder().commands(commands).build()); + DeleteSpeakerCommandsRequest retryRequest = deleteRequest.toBuilder() + .commands(removeExcessDependencies(commands)).build(); + stateMachine.getCarrier().sendSpeakerRequest(retryRequest); } else { stateMachine.addFailedCommand(commandId, response); stateMachine.removePendingCommand(commandId); diff --git a/src-java/rule-manager/rule-manager-api/src/main/java/org/openkilda/rulemanager/FlowSpeakerData.java b/src-java/rule-manager/rule-manager-api/src/main/java/org/openkilda/rulemanager/FlowSpeakerData.java index 9d734de434c..4e6adf180e3 100644 --- a/src-java/rule-manager/rule-manager-api/src/main/java/org/openkilda/rulemanager/FlowSpeakerData.java +++ b/src-java/rule-manager/rule-manager-api/src/main/java/org/openkilda/rulemanager/FlowSpeakerData.java @@ -38,7 +38,7 @@ @EqualsAndHashCode(callSuper = true) @Value -@SuperBuilder +@SuperBuilder(toBuilder = true) @JsonNaming(SnakeCaseStrategy.class) @ToString(callSuper = true) public class FlowSpeakerData extends SpeakerData { diff --git a/src-java/rule-manager/rule-manager-api/src/main/java/org/openkilda/rulemanager/GroupSpeakerData.java b/src-java/rule-manager/rule-manager-api/src/main/java/org/openkilda/rulemanager/GroupSpeakerData.java index a6b84c61756..371c2adeb5b 100644 --- a/src-java/rule-manager/rule-manager-api/src/main/java/org/openkilda/rulemanager/GroupSpeakerData.java +++ b/src-java/rule-manager/rule-manager-api/src/main/java/org/openkilda/rulemanager/GroupSpeakerData.java @@ -36,7 +36,7 @@ @EqualsAndHashCode(callSuper = true) @Value @JsonSerialize -@SuperBuilder +@SuperBuilder(toBuilder = true) @JsonNaming(SnakeCaseStrategy.class) public class GroupSpeakerData extends SpeakerData { GroupId groupId; diff --git a/src-java/rule-manager/rule-manager-api/src/main/java/org/openkilda/rulemanager/MeterSpeakerData.java b/src-java/rule-manager/rule-manager-api/src/main/java/org/openkilda/rulemanager/MeterSpeakerData.java index fd200f717eb..a30071f70b8 100644 --- a/src-java/rule-manager/rule-manager-api/src/main/java/org/openkilda/rulemanager/MeterSpeakerData.java +++ b/src-java/rule-manager/rule-manager-api/src/main/java/org/openkilda/rulemanager/MeterSpeakerData.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategy.SnakeCaseStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.EqualsAndHashCode; +import lombok.ToString; import lombok.Value; import lombok.experimental.SuperBuilder; @@ -33,8 +34,9 @@ @EqualsAndHashCode(callSuper = true) @Value -@SuperBuilder +@SuperBuilder(toBuilder = true) @JsonNaming(SnakeCaseStrategy.class) +@ToString(callSuper = true) public class MeterSpeakerData extends SpeakerData { private static final long INACCURATE_RATE_ALLOWED_DEVIATION = 1; diff --git a/src-java/rule-manager/rule-manager-api/src/main/java/org/openkilda/rulemanager/SpeakerData.java b/src-java/rule-manager/rule-manager-api/src/main/java/org/openkilda/rulemanager/SpeakerData.java index 451744edc90..76be224f68b 100644 --- a/src-java/rule-manager/rule-manager-api/src/main/java/org/openkilda/rulemanager/SpeakerData.java +++ b/src-java/rule-manager/rule-manager-api/src/main/java/org/openkilda/rulemanager/SpeakerData.java @@ -33,7 +33,7 @@ @JsonSerialize @Getter -@SuperBuilder +@SuperBuilder(toBuilder = true) @AllArgsConstructor @JsonNaming(SnakeCaseStrategy.class) @EqualsAndHashCode(of = {"switchId", "ofVersion"}) From 40fcb141f6989c6ff4a93e45546979117ba47954 Mon Sep 17 00:00:00 2001 From: Dmitry Poltavets Date: Wed, 16 Feb 2022 17:30:55 +0400 Subject: [PATCH 05/11] Fix the minimum bandwidth value for the flow. --- .../flowhs/validation/FlowValidator.java | 23 ++++++++++++-- .../flowhs/validation/YFlowValidator.java | 31 +++++++++++++++++-- 2 files changed, 50 insertions(+), 4 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 ffca4942e1b..b70e2884834 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 @@ -202,10 +202,29 @@ public void validateForSwapEndpoints(RequestedFlow firstFlow, RequestedFlow seco } @VisibleForTesting - void checkBandwidth(RequestedFlow flow) throws InvalidFlowException { + void checkBandwidth(RequestedFlow flow) throws InvalidFlowException, UnavailableFlowEndpointException { if (flow.getBandwidth() < 0) { throw new InvalidFlowException( - format("The flow '%s' has invalid bandwidth %d provided.", + format("The flow '%s' has invalid bandwidth %d provided. Bandwidth cannot be less than 0 kbps.", + flow.getFlowId(), + flow.getBandwidth()), + ErrorType.DATA_INVALID); + } + + Switch srcSwitch = switchRepository.findById(flow.getSrcSwitch()) + .orElseThrow(() -> new UnavailableFlowEndpointException(format("Endpoint switch not found %s", + flow.getSrcSwitch()))); + + Switch destSwitch = switchRepository.findById(flow.getDestSwitch()) + .orElseThrow(() -> new UnavailableFlowEndpointException(format("Endpoint switch not found %s", + flow.getDestSwitch()))); + + if ((Switch.isNoviflowSwitch(srcSwitch.getOfDescriptionSoftware()) + || Switch.isNoviflowSwitch(destSwitch.getOfDescriptionSoftware())) + && flow.getBandwidth() < 64) { + // Min rate that the NoviFlow switches allows is 64 kbps. + throw new InvalidFlowException( + format("The flow '%s' has invalid bandwidth %d provided. Bandwidth cannot be less than 64 kbps.", flow.getFlowId(), flow.getBandwidth()), ErrorType.DATA_INVALID); diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/validation/YFlowValidator.java b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/validation/YFlowValidator.java index c673f116f54..335b5d29541 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/validation/YFlowValidator.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/validation/YFlowValidator.java @@ -20,7 +20,9 @@ import org.openkilda.messaging.command.yflow.SubFlowDto; import org.openkilda.messaging.command.yflow.YFlowRequest; import org.openkilda.messaging.error.ErrorType; +import org.openkilda.model.Switch; import org.openkilda.persistence.PersistenceManager; +import org.openkilda.persistence.repositories.SwitchRepository; import org.openkilda.wfm.topology.flowhs.mapper.YFlowRequestMapper; import org.openkilda.wfm.topology.flowhs.model.RequestedFlow; @@ -32,9 +34,11 @@ */ public class YFlowValidator { private final FlowValidator flowValidator; + private final SwitchRepository switchRepository; public YFlowValidator(PersistenceManager persistenceManager) { flowValidator = new FlowValidator(persistenceManager); + this.switchRepository = persistenceManager.getRepositoryFactory().createSwitchRepository(); } /** @@ -126,10 +130,33 @@ private void checkNoOneSwitchFlow(YFlowRequest yFlowRequest) throws InvalidFlowE } } - private void checkBandwidth(YFlowRequest yFlowRequest) throws InvalidFlowException { + private void checkBandwidth(YFlowRequest yFlowRequest) + throws InvalidFlowException, UnavailableFlowEndpointException { if (yFlowRequest.getMaximumBandwidth() < 0) { throw new InvalidFlowException( - format("The y-flow %s has invalid bandwidth %d provided.", + format("The y-flow %s has invalid bandwidth %d provided. Bandwidth cannot be less than 0 kbps.", + yFlowRequest.getYFlowId(), + yFlowRequest.getMaximumBandwidth()), + ErrorType.DATA_INVALID); + } + + Switch sharedSwitch = switchRepository.findById(yFlowRequest.getSharedEndpoint().getSwitchId()) + .orElseThrow(() -> new UnavailableFlowEndpointException(format("Endpoint switch not found %s", + yFlowRequest.getSharedEndpoint().getSwitchId()))); + + boolean isNoviFlowSwitch = Switch.isNoviflowSwitch(sharedSwitch.getOfDescriptionSoftware()); + + for (SubFlowDto subFlow : yFlowRequest.getSubFlows()) { + Switch switchId = switchRepository.findById(subFlow.getEndpoint().getSwitchId()) + .orElseThrow(() -> new UnavailableFlowEndpointException(format("Endpoint switch not found %s", + subFlow.getEndpoint().getSwitchId()))); + isNoviFlowSwitch |= Switch.isNoviflowSwitch(switchId.getOfDescriptionSoftware()); + } + + if (isNoviFlowSwitch && yFlowRequest.getMaximumBandwidth() < 64) { + // Min rate that the NoviFlow switches allows is 64 kbps. + throw new InvalidFlowException( + format("The flow '%s' has invalid bandwidth %d provided. Bandwidth cannot be less than 64 kbps.", yFlowRequest.getYFlowId(), yFlowRequest.getMaximumBandwidth()), ErrorType.DATA_INVALID); From 7b08478b4f0a1026bd6a1fc6aee0a23992e965d5 Mon Sep 17 00:00:00 2001 From: Dmitriy Bogun Date: Wed, 19 Jan 2022 20:41:09 +0200 Subject: [PATCH 06/11] Rework events routing inside SwitchManagerHub Add toolset responsible for managing routing tag on each code/stack level i.e. each level hub/service/handler will have it's own tag. Dispatch tags are added automatically during delivery message to the transport level. These tags are unwrapped(on each code level) and used during for response dispatching. --- docs/design/messaging/message-cookie.md | 95 ++++++ docs/design/messaging/message-round-trip.png | Bin 0 -> 257274 bytes docs/design/messaging/message-round-trip.puml | 52 ++++ .../java/org/openkilda/messaging/Message.java | 31 +- .../openkilda/messaging/MessageCookie.java | 64 ++++ .../messaging/command/CommandMessage.java | 22 +- .../openkilda/messaging/ctrl/CtrlRequest.java | 2 +- .../messaging/ctrl/CtrlResponse.java | 2 +- .../messaging/error/ErrorMessage.java | 18 +- .../openkilda/messaging/info/InfoMessage.java | 30 +- .../messaging/info/meter/SwitchMeterData.java | 21 -- .../info/meter/SwitchMeterEntries.java | 3 +- .../info/meter/SwitchMeterUnsupported.java | 3 +- .../wfm/error/MessageDispatchException.java | 40 +++ .../wfm/error/UnexpectedInputException.java | 36 +++ .../speaker/messaging/MessageProcessor.java | 15 +- .../switchmanager/bolt/SpeakerWorkerBolt.java | 9 +- .../switchmanager/bolt/SwitchManagerHub.java | 282 +++++++++++------- .../error/OperationTimeoutException.java | 9 - .../switchmanager/fsm/SwitchSyncFsm.java | 11 +- .../switchmanager/fsm/SwitchValidateFsm.java | 16 +- .../service/CreateLagPortService.java | 133 ++++++++- .../service/DeleteLagPortService.java | 131 +++++++- .../service/SwitchManagerCarrier.java | 10 + .../SwitchManagerCarrierCookieDecorator.java | 82 +++++ .../service/SwitchManagerHubService.java | 41 +++ .../service/SwitchRuleService.java | 249 ++++++++++++++-- .../service/SwitchSyncService.java | 201 ++++++++++--- .../service/SwitchValidateService.java | 189 +++++++++++- .../service/impl/SpeakerWorkerService.java | 33 +- .../service/impl/SwitchRuleServiceImpl.java | 231 -------------- .../fsmhandlers/CreateLagPortServiceImpl.java | 153 ---------- .../fsmhandlers/DeleteLagPortServiceImpl.java | 152 ---------- .../fsmhandlers/SwitchSyncServiceImpl.java | 271 ----------------- .../SwitchValidateServiceImpl.java | 195 ------------ ...plTest.java => SwitchSyncServiceTest.java} | 92 ++---- ...st.java => SwitchValidateServiceTest.java} | 78 ++--- 37 files changed, 1618 insertions(+), 1384 deletions(-) create mode 100644 docs/design/messaging/message-cookie.md create mode 100644 docs/design/messaging/message-round-trip.png create mode 100644 docs/design/messaging/message-round-trip.puml create mode 100644 src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/MessageCookie.java delete mode 100644 src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/meter/SwitchMeterData.java create mode 100644 src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/error/MessageDispatchException.java create mode 100644 src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/error/UnexpectedInputException.java create mode 100644 src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchManagerCarrierCookieDecorator.java create mode 100644 src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchManagerHubService.java delete mode 100644 src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/SwitchRuleServiceImpl.java delete mode 100644 src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/CreateLagPortServiceImpl.java delete mode 100644 src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/DeleteLagPortServiceImpl.java delete mode 100644 src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/SwitchSyncServiceImpl.java delete mode 100644 src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/SwitchValidateServiceImpl.java rename src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/{impl/fsmhandlers/SwitchSyncServiceImplTest.java => SwitchSyncServiceTest.java} (79%) rename src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/{impl/fsmhandlers/SwitchValidateServiceImplTest.java => SwitchValidateServiceTest.java} (78%) diff --git a/docs/design/messaging/message-cookie.md b/docs/design/messaging/message-cookie.md new file mode 100644 index 00000000000..5dcc50ef4e3 --- /dev/null +++ b/docs/design/messaging/message-cookie.md @@ -0,0 +1,95 @@ +# Message cookie + +## Concept +The goal of this approach is to be able to route response into specific handler (the handler that have produced request) +into specific service into specific application/storm-bolt. + +The message producer must include into the message info unambiguously describing the sender (into hierarchical manner) - +the message cookie object. Message recipient must produce response and copy message cookie from request. So the only +task for message recipient in this approach is to copy message cookie from request into response. + +Message cookie hierarchy represent/mirror code hierarchy that produce this cookie - for example, from innermost to +outermost layers: `unique-request-id => unique-handler-id => service-id`. During message producing each code layer must +add corresponding cookie layer. + +Message cookie must be placed into message envelope (`BaseMessage` and inheritors in our terminology). It will simplify +its copying on recipient side - message cookie will not be passed into code that handle request itself but copied on the +level responsible for message decoding/encoding. At same time on request producer side, carrier decorators can add +extra message cookie layers during delivering message payload into message ejecting level without any interaction with +message payload object. + +Keeping all dispatch info into request/response objects relief from keeping it somewhere on request side and as result +no need to track it for invalidation and obsolescence. + +![route trip time sequence diagram](message-round-trip.png) + +## Wrapping process + +Cookie wrapping or layer adding process can be done by carrier(objects responsible for communication with transport layer) +decorators. I.e. application produce service and provide carrier object decorated with object that will add service +name cookie layer to each produced requests. + +For example, we have an interface defining some carrier that will be used by service and by handlers inside service: + +```java +public interface SomeCarrier { + void sendCommandToSpeaker(CommandData command, @NonNull MessageCookie cookie); +} +``` + +We can define a decorator that adds some cookie layer during request to the `sendSpeakerCommand`. + +```java +public class SomeCarrierDecorator implements SomeCarrier { + private final SomeCarrier target; + private final String layerValue; + + public SomeCarrierDecorator(SomeCarrier target, String layerValue) { + this.target = target; + this.layerValue = layerValue; + } + + public void sendCommandToSpeaker(CommandData command, @NonNull MessageCookie cookie) { + target.sendCommandToSpeaker(command, new MessageCookie(layerValue, cookie)); + } +} +``` + +And on application init we will create service in this way: + +```java +public class App { + Map services = new Map<>(); + + public void init() { + SomeService someService = new SomeService(new SomeCarrierDecorator(carrier, "some_service_name")); + serivces.put("some_service_name", someService); + } +} +``` + +So on response we can locate required service in this way: + +```java +public class App { + Map services = new Map<>(); + + public void dispatch(MessageData payload, MessageCookie cookie) { + ServiceBase service = services.get(cookie.getValue()); + if (service != null) { + service.dispatch(payload, cookie.getNested()); + } else { + log.error("dispatch error"); + } + } +} +``` + +Service itself can add one more decorator to the carrier, responsible for adding handler unique id cookie layer. And +dispatching can be continued inside service. So each code layer process it's own message cookie layer and fully control +the meaning of cookie value (on specific layer). + +## Some possible tooling improvements + +`MessageCookie` can define equals/hash methods that do not take into account value of nested field/cookie. In this case +cookie object itself can be used as `Map` key during dispatch. diff --git a/docs/design/messaging/message-round-trip.png b/docs/design/messaging/message-round-trip.png new file mode 100644 index 0000000000000000000000000000000000000000..897ce404f31165630ee3df629eacbd880f08814a GIT binary patch literal 257274 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 rUfvAWZnK literal 0 HcmV?d00001 diff --git a/docs/design/messaging/message-round-trip.puml b/docs/design/messaging/message-round-trip.puml new file mode 100644 index 00000000000..df952a3a893 --- /dev/null +++ b/docs/design/messaging/message-round-trip.puml @@ -0,0 +1,52 @@ +@startuml +title message round trip + +collections handler +collections service +participant application +participant transport +participant remote +collections remote_handler + +activate handler +handler -> handler : produce message payload\nand message cookie\nwith unique request id\n[payload, cookie[0]] +handler -> service : [payload, cookie[0]] +deactivate handler +service -> application : add handler-id layer\n[payload, cookie[1]] +application -> transport : add service name layer\n[payload, cookie[2]] + +activate transport +transport -> transport : [payload, cookie[2]] => [request_message] +transport -> remote: [request_message] +deactivate transport + +activate remote +remote -> remote : [request_message] => [payload, cookie[2]] +remote -> remote_handler : [payload] +activate remote_handler +remote_handler -> remote : [response] +deactivate remote_handler +remote -> remote : response + cookie[2] => [response_message] +remote -> transport : [response_message] +deactivate remote + +activate transport +transport -> transport : [response_message] => [response, cookie[2]] +transport -> application : [response, cookie[2]] +deactivate transport + +activate application +application -> application : select service by cookie[2]\nunwrap +application -> service : [response, cookie[1]] +deactivate application + +activate service +service -> service : select handler by cookie[1]\nunwrap +service -> handler : [response, cookie[0]] +deactivate service + +activate handler +handler -> handler : identify request by cookie[0]\nhandle response +deactivate handler + +@enduml diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/Message.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/Message.java index ede7e961a64..1a4ae39a47b 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/Message.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/Message.java @@ -23,6 +23,8 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; /** * Class represents high level view of every message used by any service. @@ -47,20 +49,23 @@ public class Message extends BaseMessage { @JsonProperty(DESTINATION) protected Destination destination; + @Getter + @Setter + @JsonProperty("cookie") + protected MessageCookie cookie; + /** * Instance constructor. * * @param timestamp message timestamp * @param correlationId message correlation id - * @param destination message destination */ - @JsonCreator - public Message(@JsonProperty(TIMESTAMP) final long timestamp, - @JsonProperty(CORRELATION_ID) final String correlationId, - @JsonProperty(DESTINATION) final Destination destination) { - super(timestamp); - this.correlationId = correlationId; - this.destination = destination; + public Message(final long timestamp, final String correlationId) { + this(timestamp, correlationId, null); + } + + public Message(final long timestamp, final String correlationId, MessageCookie cookie) { + this(timestamp, correlationId, null, cookie); } /** @@ -68,10 +73,17 @@ public Message(@JsonProperty(TIMESTAMP) final long timestamp, * * @param timestamp message timestamp * @param correlationId message correlation id + * @param destination message destination */ - public Message(final long timestamp, final String correlationId) { + @JsonCreator + public Message(@JsonProperty(TIMESTAMP) final long timestamp, + @JsonProperty(CORRELATION_ID) final String correlationId, + @JsonProperty(DESTINATION) final Destination destination, + @JsonProperty("cookie") MessageCookie cookie) { super(timestamp); this.correlationId = correlationId; + this.destination = destination; + this.cookie = cookie; } /** @@ -109,6 +121,7 @@ public String toString() { return toStringHelper(this) .add(TIMESTAMP, timestamp) .add(CORRELATION_ID, correlationId) + .add("cookie", cookie) .add(DESTINATION, destination) .toString(); } diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/MessageCookie.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/MessageCookie.java new file mode 100644 index 00000000000..aa3bac23ce1 --- /dev/null +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/MessageCookie.java @@ -0,0 +1,64 @@ +/* Copyright 2022 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; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.apache.commons.lang3.StringUtils; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +@Value +@EqualsAndHashCode +public class MessageCookie implements Serializable { + private static final String FLAT_VIEW_SEPARATOR = " : "; + + @JsonProperty("value") + String value; + + @EqualsAndHashCode.Exclude + @JsonProperty("nested") + MessageCookie nested; + + public MessageCookie(String value) { + this(value, null); + } + + @JsonCreator + public MessageCookie( + @JsonProperty("value") String value, + @JsonProperty("nested") MessageCookie nested) { + this.value = value; + this.nested = nested; + } + + /** + * Flat {@link MessageCookie} representation. Do not supposed to be parsable. + */ + public String toString() { + List payload = new ArrayList<>(); + MessageCookie entry = this; + while (entry != null) { + payload.add(entry.value); + entry = entry.nested; + } + return "{ " + StringUtils.joinWith(FLAT_VIEW_SEPARATOR, payload.toArray()) + " }"; + } +} diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/command/CommandMessage.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/command/CommandMessage.java index 9b298b920bd..d3a627b5d1a 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/command/CommandMessage.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/command/CommandMessage.java @@ -23,6 +23,7 @@ import org.openkilda.messaging.Destination; import org.openkilda.messaging.Message; +import org.openkilda.messaging.MessageCookie; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; @@ -64,16 +65,22 @@ public class CommandMessage extends Message { public CommandMessage(@JsonProperty(PAYLOAD) final CommandData data, @JsonProperty(TIMESTAMP) final long timestamp, @JsonProperty(CORRELATION_ID) final String correlationId, - @JsonProperty(DESTINATION) final Destination destination) { - super(timestamp, correlationId, destination); + @JsonProperty(DESTINATION) final Destination destination, + @JsonProperty("cookie") MessageCookie cookie) { + super(timestamp, correlationId, destination, cookie); setData(data); } - public CommandMessage(final CommandData data, - final long timestamp, - final String correlationId) { - super(timestamp, correlationId); - setData(data); + public CommandMessage(final CommandData data, final long timestamp, final String correlationId) { + this(data, timestamp, correlationId, null, null); + } + + public CommandMessage(CommandData data, long timestamp, String correlationId, Destination destination) { + this(data, timestamp, correlationId, destination, null); + } + + public CommandMessage(CommandData data, String correlationId, MessageCookie cookie) { + this(data, System.currentTimeMillis(), correlationId, null, cookie); } /** @@ -102,6 +109,7 @@ public String toString() { return toStringHelper(this) .add(TIMESTAMP, timestamp) .add(CORRELATION_ID, correlationId) + .add("cookie", cookie) .add(DESTINATION, destination) .add(PAYLOAD, data) .toString(); diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/ctrl/CtrlRequest.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/ctrl/CtrlRequest.java index f021b94ad51..34bcc7f0685 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/ctrl/CtrlRequest.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/ctrl/CtrlRequest.java @@ -70,7 +70,7 @@ public CtrlRequest(@JsonProperty(ROUTE) String route, @JsonProperty(TIMESTAMP) final long timestamp, @JsonProperty(CORRELATION_ID) final String correlationId, @JsonProperty(DESTINATION) final Destination destination) { - super(timestamp, correlationId, destination); + super(timestamp, correlationId, destination, null); this.route = route; this.data = data; } diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/ctrl/CtrlResponse.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/ctrl/CtrlResponse.java index a8ad56d0d88..86d8bee8b7d 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/ctrl/CtrlResponse.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/ctrl/CtrlResponse.java @@ -48,7 +48,7 @@ public CtrlResponse(@JsonProperty(PAYLOAD) ResponseData data, @JsonProperty(TIMESTAMP) final long timestamp, @JsonProperty(CORRELATION_ID) final String correlationId, @JsonProperty(DESTINATION) final Destination destination) { - super(timestamp, correlationId, destination); + super(timestamp, correlationId, destination, null); this.data = data; } diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/error/ErrorMessage.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/error/ErrorMessage.java index 4c51cf86190..903f3ffe975 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/error/ErrorMessage.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/error/ErrorMessage.java @@ -23,6 +23,7 @@ import org.openkilda.messaging.Destination; import org.openkilda.messaging.Message; +import org.openkilda.messaging.MessageCookie; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; @@ -64,8 +65,9 @@ public class ErrorMessage extends Message { public ErrorMessage(@JsonProperty(PAYLOAD) final ErrorData data, @JsonProperty(TIMESTAMP) final long timestamp, @JsonProperty(CORRELATION_ID) final String correlationId, - @JsonProperty(DESTINATION) final Destination destination) { - super(timestamp, correlationId, destination); + @JsonProperty(DESTINATION) final Destination destination, + @JsonProperty("cookie") MessageCookie cookie) { + super(timestamp, correlationId, destination, cookie); setData(data); } @@ -79,8 +81,15 @@ public ErrorMessage(@JsonProperty(PAYLOAD) final ErrorData data, public ErrorMessage(final ErrorData data, final long timestamp, final String correlationId) { - super(timestamp, correlationId); - setData(data); + this(data, timestamp, correlationId, null, null); + } + + public ErrorMessage(ErrorData data, long timestamp, String correlationId, Destination destination) { + this(data, timestamp, correlationId, destination, null); + } + + public ErrorMessage(ErrorData data, String correlationId, MessageCookie cookie) { + this(data, System.currentTimeMillis(), correlationId, null, cookie); } /** @@ -109,6 +118,7 @@ public String toString() { return toStringHelper(this) .add(TIMESTAMP, timestamp) .add(CORRELATION_ID, correlationId) + .add("cookie", cookie) .add(DESTINATION, destination) .add(PAYLOAD, data) .toString(); diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/InfoMessage.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/InfoMessage.java index 5cfa69641f0..436a6dc7a18 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/InfoMessage.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/InfoMessage.java @@ -25,6 +25,7 @@ import org.openkilda.bluegreen.kafka.TransportErrorReport; import org.openkilda.messaging.Destination; import org.openkilda.messaging.Message; +import org.openkilda.messaging.MessageCookie; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -71,12 +72,17 @@ public InfoMessage(@JsonProperty(PAYLOAD) final InfoData data, @JsonProperty(TIMESTAMP) final long timestamp, @JsonProperty(CORRELATION_ID) final String correlationId, @JsonProperty(DESTINATION) final Destination destination, - @JsonProperty(REGION) final String region) { - super(timestamp, correlationId, destination); + @JsonProperty(REGION) final String region, + @JsonProperty("cookie") MessageCookie cookie) { + super(timestamp, correlationId, destination, cookie); this.region = region; this.data = data; } + public InfoMessage(InfoData data, long timestamp, String correlationId, Destination destination, String region) { + this(data, timestamp, correlationId, destination, region, null); + } + /** * Instance constructor. * @@ -84,11 +90,8 @@ public InfoMessage(@JsonProperty(PAYLOAD) final InfoData data, * @param timestamp timestamp value * @param correlationId message correlation id */ - public InfoMessage(final InfoData data, - final long timestamp, - final String correlationId) { - super(timestamp, correlationId); - this.data = data; + public InfoMessage(final InfoData data, final long timestamp, final String correlationId) { + this(data, timestamp, correlationId, null, null, null); } /** @@ -99,13 +102,12 @@ public InfoMessage(final InfoData data, * @param correlationId message correlation id * @param region floodlight region identifier */ - public InfoMessage(final InfoData data, - final long timestamp, - final String correlationId, - final String region) { - super(timestamp, correlationId); - this.data = data; - this.region = region; + public InfoMessage(final InfoData data, final long timestamp, final String correlationId, final String region) { + this(data, timestamp, correlationId, null, region, null); + } + + public InfoMessage(InfoData data, String correlationId, MessageCookie cookie) { + this(data, System.currentTimeMillis(), correlationId, null, null, cookie); } @JsonIgnore diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/meter/SwitchMeterData.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/meter/SwitchMeterData.java deleted file mode 100644 index 42c4fcbe1f3..00000000000 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/meter/SwitchMeterData.java +++ /dev/null @@ -1,21 +0,0 @@ -/* 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.info.meter; - -import org.openkilda.messaging.info.InfoData; - -public class SwitchMeterData extends InfoData { -} diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/meter/SwitchMeterEntries.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/meter/SwitchMeterEntries.java index c46303e61eb..d0b8ec1d311 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/meter/SwitchMeterEntries.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/meter/SwitchMeterEntries.java @@ -15,6 +15,7 @@ package org.openkilda.messaging.info.meter; +import org.openkilda.messaging.info.InfoData; import org.openkilda.model.SwitchId; import com.fasterxml.jackson.annotation.JsonCreator; @@ -28,7 +29,7 @@ @Value @Builder @EqualsAndHashCode(callSuper = false) -public class SwitchMeterEntries extends SwitchMeterData { +public class SwitchMeterEntries extends InfoData { @JsonProperty(value = "switch_id") private SwitchId switchId; diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/meter/SwitchMeterUnsupported.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/meter/SwitchMeterUnsupported.java index da108f493f4..82e79f1e0c9 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/meter/SwitchMeterUnsupported.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/meter/SwitchMeterUnsupported.java @@ -15,6 +15,7 @@ package org.openkilda.messaging.info.meter; +import org.openkilda.messaging.info.InfoData; import org.openkilda.model.SwitchId; import com.fasterxml.jackson.annotation.JsonCreator; @@ -26,7 +27,7 @@ @Value @Builder @EqualsAndHashCode(callSuper = false) -public class SwitchMeterUnsupported extends SwitchMeterData { +public class SwitchMeterUnsupported extends InfoData { @JsonProperty(value = "switch_id") private SwitchId switchId; diff --git a/src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/error/MessageDispatchException.java b/src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/error/MessageDispatchException.java new file mode 100644 index 00000000000..22c27ac564c --- /dev/null +++ b/src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/error/MessageDispatchException.java @@ -0,0 +1,40 @@ +/* Copyright 2022 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.error; + +import org.openkilda.messaging.MessageCookie; + +public class MessageDispatchException extends Exception { + private final MessageCookie cookie; + + public MessageDispatchException(MessageCookie cookie) { + this.cookie = cookie; + } + + public MessageDispatchException() { + this(null); + } + + @Override + public String getMessage() { + return String.format("Unable to dispatch message (unprocessed cookie part: %s)", cookie); + } + + public String getMessage(MessageCookie rootCookie) { + return String.format( + "Unable to dispatch message (unprocessed cookie part: %s, root cookie: %s)", cookie, rootCookie); + } +} diff --git a/src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/error/UnexpectedInputException.java b/src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/error/UnexpectedInputException.java new file mode 100644 index 00000000000..8a00e18fdff --- /dev/null +++ b/src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/error/UnexpectedInputException.java @@ -0,0 +1,36 @@ +/* Copyright 2022 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.error; + +import org.openkilda.messaging.Message; +import org.openkilda.messaging.MessageCookie; +import org.openkilda.messaging.MessageData; + +public class UnexpectedInputException extends Exception { + private final Object input; + + public UnexpectedInputException(Message input) { + this.input = input; + } + + public UnexpectedInputException(MessageData payload) { + this.input = payload; + } + + public String getMessage(MessageCookie rootCookie) { + return String.format("Got unexpected input data with cookie %s: %s", rootCookie, input); + } +} diff --git a/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/messaging/MessageProcessor.java b/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/messaging/MessageProcessor.java index fcbb776bbe1..c7c79205fe1 100644 --- a/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/messaging/MessageProcessor.java +++ b/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/messaging/MessageProcessor.java @@ -20,6 +20,7 @@ import org.openkilda.grpc.speaker.mapper.RequestMapper; import org.openkilda.grpc.speaker.service.GrpcSenderService; import org.openkilda.messaging.Message; +import org.openkilda.messaging.MessageCookie; import org.openkilda.messaging.MessageData; import org.openkilda.messaging.command.CommandData; import org.openkilda.messaging.command.CommandMessage; @@ -101,7 +102,7 @@ private void handleCommandMessage(CommandMessage command, String key) { result = unhandledMessage(command); } - result.thenAccept(response -> sendResponse(response, correlationId, key)); + result.thenAccept(response -> sendResponse(response, correlationId, key, command.getCookie())); } private CompletableFuture handleCreateLogicalPortRequest(CreateLogicalPortRequest request) { @@ -186,16 +187,18 @@ private MessageData handleError(Throwable error, String actionDefinition) { } } - private void sendResponse(Response response, String correlationId, String key) { - Message message = makeMessage(response.getPayload(), correlationId); + private void sendResponse(Response response, String correlationId, String key, MessageCookie cookie) { + log.debug("GRPC speaker is sending response on request {} with cookie {}: {}", key, cookie, response); + Message message = makeMessage(response, correlationId, cookie); messageProducer.send(response.getTopic(), key, message); } - private Message makeMessage(MessageData payload, String correlationId) { + private Message makeMessage(Response response, String correlationId, MessageCookie cookie) { + MessageData payload = response.getPayload(); if (payload instanceof InfoData) { - return new InfoMessage((InfoData) payload, System.currentTimeMillis(), correlationId); + return new InfoMessage((InfoData) payload, correlationId, cookie); } else if (payload instanceof ErrorData) { - return new ErrorMessage((ErrorData) payload, System.currentTimeMillis(), correlationId); + return new ErrorMessage((ErrorData) payload, correlationId, cookie); } else { throw new IllegalArgumentException(String.format( "Unexpected/unsupported message payload type: %s", payload.getClass().getName())); diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/bolt/SpeakerWorkerBolt.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/bolt/SpeakerWorkerBolt.java index d5e136124ac..053564c1e4c 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/bolt/SpeakerWorkerBolt.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/bolt/SpeakerWorkerBolt.java @@ -16,6 +16,7 @@ package org.openkilda.wfm.topology.switchmanager.bolt; import org.openkilda.messaging.Message; +import org.openkilda.messaging.MessageCookie; import org.openkilda.messaging.command.CommandData; import org.openkilda.messaging.command.CommandMessage; import org.openkilda.messaging.command.grpc.GrpcBaseRequest; @@ -34,6 +35,9 @@ public class SpeakerWorkerBolt extends WorkerBolt implements SpeakerCommandCarri public static final String ID = "speaker.worker.bolt"; public static final String INCOME_STREAM = "speaker.worker.stream"; + + public static final String FIELD_ID_COOKIE = "cookie"; + private transient SpeakerWorkerService service; public SpeakerWorkerBolt(Config config) { @@ -50,11 +54,12 @@ protected void init() { protected void onHubRequest(Tuple input) throws PipelineException { String key = input.getStringByField(MessageKafkaTranslator.FIELD_ID_KEY); CommandData command = pullValue(input, MessageKafkaTranslator.FIELD_ID_PAYLOAD, CommandData.class); + MessageCookie cookie = pullValue(input, FIELD_ID_COOKIE, MessageCookie.class); if (command instanceof GrpcBaseRequest) { - service.sendGrpcCommand(key, command); + service.sendGrpcCommand(key, command, cookie); } else { - service.sendFloodlightCommand(key, command); + service.sendFloodlightCommand(key, command, cookie); } } diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/bolt/SwitchManagerHub.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/bolt/SwitchManagerHub.java index bb7e5076cb4..fd2fac89e8c 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/bolt/SwitchManagerHub.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/bolt/SwitchManagerHub.java @@ -17,6 +17,7 @@ import org.openkilda.bluegreen.LifecycleEvent; import org.openkilda.messaging.Message; +import org.openkilda.messaging.MessageCookie; import org.openkilda.messaging.command.CommandData; import org.openkilda.messaging.command.CommandMessage; import org.openkilda.messaging.command.switches.SwitchRulesDeleteRequest; @@ -27,29 +28,14 @@ import org.openkilda.messaging.error.ErrorType; import org.openkilda.messaging.info.InfoData; import org.openkilda.messaging.info.InfoMessage; -import org.openkilda.messaging.info.flow.FlowDumpResponse; -import org.openkilda.messaging.info.flow.FlowInstallResponse; -import org.openkilda.messaging.info.flow.FlowReinstallResponse; -import org.openkilda.messaging.info.flow.FlowRemoveResponse; -import org.openkilda.messaging.info.group.GroupDumpResponse; -import org.openkilda.messaging.info.grpc.CreateLogicalPortResponse; -import org.openkilda.messaging.info.grpc.DeleteLogicalPortResponse; -import org.openkilda.messaging.info.grpc.DumpLogicalPortsResponse; -import org.openkilda.messaging.info.meter.MeterDumpResponse; -import org.openkilda.messaging.info.meter.SwitchMeterData; -import org.openkilda.messaging.info.meter.SwitchMeterUnsupported; -import org.openkilda.messaging.info.switches.DeleteGroupResponse; -import org.openkilda.messaging.info.switches.DeleteMeterResponse; -import org.openkilda.messaging.info.switches.InstallGroupResponse; -import org.openkilda.messaging.info.switches.ModifyGroupResponse; -import org.openkilda.messaging.info.switches.ModifyMeterResponse; -import org.openkilda.messaging.info.switches.SwitchRulesResponse; import org.openkilda.messaging.swmanager.request.CreateLagPortRequest; import org.openkilda.messaging.swmanager.request.DeleteLagPortRequest; import org.openkilda.persistence.PersistenceManager; import org.openkilda.rulemanager.RuleManagerConfig; import org.openkilda.rulemanager.RuleManagerImpl; +import org.openkilda.wfm.error.MessageDispatchException; import org.openkilda.wfm.error.PipelineException; +import org.openkilda.wfm.error.UnexpectedInputException; import org.openkilda.wfm.share.flow.resources.FlowResourcesConfig; import org.openkilda.wfm.share.hubandspoke.HubBolt; import org.openkilda.wfm.share.utils.KeyProvider; @@ -57,34 +43,45 @@ import org.openkilda.wfm.share.zk.ZooKeeperBolt; import org.openkilda.wfm.topology.switchmanager.StreamType; import org.openkilda.wfm.topology.switchmanager.SwitchManagerTopologyConfig; +import org.openkilda.wfm.topology.switchmanager.error.SwitchManagerException; import org.openkilda.wfm.topology.switchmanager.model.ValidationResult; import org.openkilda.wfm.topology.switchmanager.service.CreateLagPortService; import org.openkilda.wfm.topology.switchmanager.service.DeleteLagPortService; import org.openkilda.wfm.topology.switchmanager.service.LagPortOperationConfig; import org.openkilda.wfm.topology.switchmanager.service.SwitchManagerCarrier; +import org.openkilda.wfm.topology.switchmanager.service.SwitchManagerCarrierCookieDecorator; +import org.openkilda.wfm.topology.switchmanager.service.SwitchManagerHubService; import org.openkilda.wfm.topology.switchmanager.service.SwitchRuleService; import org.openkilda.wfm.topology.switchmanager.service.SwitchSyncService; import org.openkilda.wfm.topology.switchmanager.service.SwitchValidateService; -import org.openkilda.wfm.topology.switchmanager.service.impl.SwitchRuleServiceImpl; import org.openkilda.wfm.topology.switchmanager.service.impl.ValidationServiceImpl; -import org.openkilda.wfm.topology.switchmanager.service.impl.fsmhandlers.CreateLagPortServiceImpl; -import org.openkilda.wfm.topology.switchmanager.service.impl.fsmhandlers.DeleteLagPortServiceImpl; -import org.openkilda.wfm.topology.switchmanager.service.impl.fsmhandlers.SwitchSyncServiceImpl; -import org.openkilda.wfm.topology.switchmanager.service.impl.fsmhandlers.SwitchValidateServiceImpl; import org.openkilda.wfm.topology.utils.MessageKafkaTranslator; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.apache.storm.topology.OutputFieldsDeclarer; import org.apache.storm.tuple.Fields; import org.apache.storm.tuple.Tuple; import org.apache.storm.tuple.Values; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + @Slf4j public class SwitchManagerHub extends HubBolt implements SwitchManagerCarrier { public static final String ID = "switch.manager.hub"; public static final String INCOME_STREAM = "switch.manage.command"; + public static final String FIELD_ID_COOKIE = SpeakerWorkerBolt.FIELD_ID_COOKIE; + + public static final Fields WORKER_STREAM_FIELDS = + new Fields( + MessageKafkaTranslator.FIELD_ID_KEY, MessageKafkaTranslator.FIELD_ID_PAYLOAD, FIELD_ID_COOKIE, + FIELD_ID_CONTEXT); + public static final String NORTHBOUND_STREAM_ID = StreamType.TO_NORTHBOUND.toString(); public static final Fields NORTHBOUND_STREAM_FIELDS = new Fields( MessageKafkaTranslator.FIELD_ID_KEY, MessageKafkaTranslator.FIELD_ID_PAYLOAD); @@ -96,12 +93,16 @@ public class SwitchManagerHub extends HubBolt implements SwitchManagerCarrier { private final FlowResourcesConfig flowResourcesConfig; private final SwitchManagerTopologyConfig topologyConfig; private final RuleManagerConfig ruleManagerConfig; + private transient SwitchValidateService validateService; private transient SwitchSyncService syncService; private transient SwitchRuleService switchRuleService; private transient CreateLagPortService createLagPortService; private transient DeleteLagPortService deleteLagPortService; + private transient Map timeoutDispatchMap; + private transient Map serviceRegistry; + private LifecycleEvent deferredShutdownEvent; public SwitchManagerHub(HubBolt.Config hubConfig, PersistenceManager persistenceManager, @@ -120,20 +121,34 @@ public SwitchManagerHub(HubBolt.Config hubConfig, PersistenceManager persistence public void init() { super.init(); - validateService = new SwitchValidateServiceImpl(this, persistenceManager, - new ValidationServiceImpl(persistenceManager), - new RuleManagerImpl(ruleManagerConfig)); - syncService = new SwitchSyncServiceImpl(this, persistenceManager, flowResourcesConfig); - switchRuleService = new SwitchRuleServiceImpl(this, persistenceManager.getRepositoryFactory()); - LagPortOperationConfig config = new LagPortOperationConfig( persistenceManager.getRepositoryFactory(), persistenceManager.getTransactionManager(), topologyConfig.getBfdPortOffset(), topologyConfig.getBfdPortMaxNumber(), topologyConfig.getLagPortOffset(), topologyConfig.getLagPortMaxNumber(), topologyConfig.getLagPortPoolChunksCount(), topologyConfig.getLagPortPoolCacheSize()); log.info("LAG logical ports service config: {}", config); - createLagPortService = new CreateLagPortServiceImpl(this, config); - deleteLagPortService = new DeleteLagPortServiceImpl(this, config); + + timeoutDispatchMap = new HashMap<>(); + + serviceRegistry = new HashMap<>(); + + // Service name are used by service registry will be used as part of the produced message cookies and as a + // result as delivery tag on response dispatching. This means that it must be unique across this bolt/class or + // response dispatching will fail. + validateService = registerService(serviceRegistry, "switch-validate", this, + carrier -> new SwitchValidateService( + carrier, persistenceManager, + new ValidationServiceImpl(persistenceManager), new RuleManagerImpl(ruleManagerConfig))); + syncService = registerService( + serviceRegistry, "switch-sync", this, + carrier -> new SwitchSyncService(carrier, persistenceManager, flowResourcesConfig)); + switchRuleService = registerService( + serviceRegistry, "switch-rules", this, + carrier -> new SwitchRuleService(carrier, persistenceManager.getRepositoryFactory())); + createLagPortService = registerService( + serviceRegistry, "lag-create", this, carrier -> new CreateLagPortService(carrier, config)); + deleteLagPortService = registerService( + serviceRegistry, "lag-delete", this, carrier -> new DeleteLagPortService(carrier, config)); } @Override @@ -143,93 +158,126 @@ protected void onRequest(Tuple input) throws PipelineException { return; } - String key = input.getStringByField(MessageKafkaTranslator.FIELD_ID_KEY); + String requestKey = input.getStringByField(MessageKafkaTranslator.FIELD_ID_KEY); CommandMessage message = pullValue(input, MessageKafkaTranslator.FIELD_ID_PAYLOAD, CommandMessage.class); - CommandData data = message.getData(); + CommandData request = message.getData(); + try { + if (! dispatchRequest(requestKey, request)) { + unhandledInput(input); + } + } catch (SwitchManagerException e) { + log.error("Unable to handle request {} with key {} - {}", request, requestKey, e.getMessage()); + errorResponse(requestKey, e.getError(), "Unable to handle switch manager request", e.getMessage()); + } + } + + @Override + protected void onWorkerResponse(Tuple input) throws PipelineException { + Message message = pullValue(input, MessageKafkaTranslator.FIELD_ID_PAYLOAD, Message.class); + try { + dispatchWorkerResponse(message); + } catch (MessageDispatchException e) { + log.warn("Unable to route worker response {}: {}", message, e.getMessage(message.getCookie())); + } catch (UnexpectedInputException e) { + log.error("{}", e.getMessage(message.getCookie()), e); + } + } + + @Override + protected void onTimeout(String key, Tuple tuple) throws PipelineException { + MessageCookie route = timeoutDispatchMap.remove(key); + if (route != null) { + dispatchTimeout(route); + } else { + log.info( + "Ignoring timeout notification for request key \"{}\"- there is no timeout dispatch route found " + + "(can happens due to timeout delivery/cancel race)", key); + } + } + + private boolean dispatchRequest(String key, CommandData data) { if (data instanceof SwitchValidateRequest) { - validateService.handleSwitchValidateRequest(key, (SwitchValidateRequest) data); + dispatchRequest( + validateService, key, + service -> service.handleSwitchValidateRequest(key, (SwitchValidateRequest) data)); } else if (data instanceof SwitchRulesDeleteRequest) { - switchRuleService.deleteRules(key, (SwitchRulesDeleteRequest) data); + dispatchRequest( + switchRuleService, key, service -> service.deleteRules(key, (SwitchRulesDeleteRequest) data)); } else if (data instanceof SwitchRulesInstallRequest) { - switchRuleService.installRules(key, (SwitchRulesInstallRequest) data); + dispatchRequest( + switchRuleService, key, service -> service.installRules(key, (SwitchRulesInstallRequest) data)); } else if (data instanceof CreateLagPortRequest) { - createLagPortService.handleCreateLagRequest(key, (CreateLagPortRequest) data); + dispatchRequest( + createLagPortService, key, + service -> service.handleCreateLagRequest(key, (CreateLagPortRequest) data)); } else if (data instanceof DeleteLagPortRequest) { - deleteLagPortService.handleDeleteLagRequest(key, (DeleteLagPortRequest) data); + dispatchRequest( + deleteLagPortService, key, + service -> service.handleDeleteLagRequest(key, (DeleteLagPortRequest) data)); } else { - log.warn("Receive unexpected CommandMessage for key {}: {}", key, data); + return false; } + return true; } - private void handleMetersResponse(String key, SwitchMeterData data) { - if (data instanceof SwitchMeterUnsupported) { - validateService.handleMetersUnsupportedResponse(key); - } else { - log.warn("Receive unexpected SwitchMeterData for key {}: {}", key, data); - } + private void dispatchRequest(S service, String requestKey, Consumer action) { + timeoutDispatchMap.put(requestKey, service.getCarrier().newDispatchRoute(requestKey)); + action.accept(service); } - @Override - protected void onWorkerResponse(Tuple input) throws PipelineException { - String key = KeyProvider.getParentKey(input.getStringByField(MessageKafkaTranslator.FIELD_ID_KEY)); - Message message = pullValue(input, MessageKafkaTranslator.FIELD_ID_PAYLOAD, Message.class); + private void dispatchWorkerResponse(Message message) throws MessageDispatchException, UnexpectedInputException { + MessageCookie cookie = message.getCookie(); + if (cookie == null) { + log.error( + "There is no message cookie in worker response, can't determine target service " + + "(response: {})", + message); + return; + } + + SwitchManagerHubService service = serviceRegistry.get(cookie); + if (service == null) { + throw new MessageDispatchException(cookie); + } + dispatchWorkerResponse(service, message, cookie.getNested()); + } + private void dispatchWorkerResponse(SwitchManagerHubService service, Message message, MessageCookie serviceCookie) + throws UnexpectedInputException, MessageDispatchException { if (message instanceof InfoMessage) { - InfoData data = ((InfoMessage) message).getData(); - if (data instanceof FlowDumpResponse) { - validateService.handleFlowEntriesResponse(key, (FlowDumpResponse) data); - } else if (data instanceof GroupDumpResponse) { - validateService.handleGroupEntriesResponse(key, (GroupDumpResponse) data); - } else if (data instanceof DumpLogicalPortsResponse) { - validateService.handleLogicalPortResponse(key, (DumpLogicalPortsResponse) data); - } else if (data instanceof MeterDumpResponse) { - validateService.handleMeterEntriesResponse(key, (MeterDumpResponse) data); - } else if (data instanceof SwitchMeterData) { - handleMetersResponse(key, (SwitchMeterData) data); - } else if (data instanceof FlowInstallResponse) { - syncService.handleInstallRulesResponse(key); - } else if (data instanceof FlowRemoveResponse) { - syncService.handleRemoveRulesResponse(key); - } else if (data instanceof FlowReinstallResponse) { - syncService.handleReinstallDefaultRulesResponse(key, (FlowReinstallResponse) data); - } else if (data instanceof DeleteMeterResponse) { - syncService.handleRemoveMetersResponse(key); - } else if (data instanceof ModifyMeterResponse) { - syncService.handleModifyMetersResponse(key); - } else if (data instanceof InstallGroupResponse) { - syncService.handleInstallGroupResponse(key); - } else if (data instanceof ModifyGroupResponse) { - syncService.handleModifyGroupResponse(key); - } else if (data instanceof DeleteGroupResponse) { - syncService.handleDeleteGroupResponse(key); - } else if (data instanceof SwitchRulesResponse) { - switchRuleService.rulesResponse(key, (SwitchRulesResponse) data); - } else if (data instanceof CreateLogicalPortResponse) { - createLagPortService.handleGrpcResponse(key, (CreateLogicalPortResponse) data); - syncService.handleCreateLogicalPortResponse(key); - } else if (data instanceof DeleteLogicalPortResponse) { - deleteLagPortService.handleGrpcResponse(key, (DeleteLogicalPortResponse) data); - syncService.handleDeleteLogicalPortResponse(key); - } else { - log.warn("Receive unexpected InfoData for key {}: {}", key, data); - } + dispatchWorkerResponse(service, (InfoMessage) message, serviceCookie); } else if (message instanceof ErrorMessage) { - log.warn("Receive ErrorMessage for key {}", key); - validateService.handleTaskError(key, (ErrorMessage) message); - syncService.handleTaskError(key, (ErrorMessage) message); - createLagPortService.handleTaskError(key, (ErrorMessage) message); - deleteLagPortService.handleTaskError(key, (ErrorMessage) message); + dispatchWorkerResponse(service, (ErrorMessage) message, serviceCookie); + } else { + throw new UnexpectedInputException(message); } } - @Override - public void onTimeout(String key, Tuple tuple) { - log.warn("Receive TaskTimeout for key {}", key); - validateService.handleTaskTimeout(key); - syncService.handleTaskTimeout(key); - createLagPortService.handleTaskTimeout(key); - deleteLagPortService.handleTaskTimeout(key); + private void dispatchWorkerResponse( + SwitchManagerHubService service, InfoMessage message, MessageCookie serviceCookie) + throws UnexpectedInputException, MessageDispatchException { + service.dispatchWorkerMessage(message.getData(), serviceCookie); + } + + private void dispatchWorkerResponse( + SwitchManagerHubService service, ErrorMessage message, MessageCookie serviceCookie) + throws MessageDispatchException { + service.dispatchWorkerMessage(message.getData(), serviceCookie); + } + + private void dispatchTimeout(MessageCookie cookie) { + SwitchManagerHubService service = serviceRegistry.get(cookie); + if (service == null) { + log.error("Unable to dispatch timeout into any service using cookie: {}", cookie); + return; + } + + try { + service.timeout(cookie.getNested()); + } catch (MessageDispatchException e) { + log.info("There is no handler to process timeout notification: {}", cookie); + } } @Override @@ -253,12 +301,28 @@ protected void activate() { @Override public void cancelTimeoutCallback(String key) { + timeoutDispatchMap.remove(key); cancelCallback(key); } + @Override + public MessageCookie newDispatchRoute(String requestKey) { + return new MessageCookie(requestKey); + } + @Override public void sendCommandToSpeaker(String key, CommandData command) { - emit(SpeakerWorkerBolt.INCOME_STREAM, getCurrentTuple(), makeWorkerTuple(key, command)); + // will never be user, because of carrier decorator + sendCommandToSpeaker(command, new MessageCookie(key)); + } + + @Override + public void sendCommandToSpeaker(CommandData command, @NonNull MessageCookie cookie) { + sendCommandToSpeaker(cookie.toString(), command, cookie); + } + + public void sendCommandToSpeaker(String requestKey, CommandData command, MessageCookie cookie) { + emit(SpeakerWorkerBolt.INCOME_STREAM, getCurrentTuple(), makeWorkerTuple(requestKey, command, cookie)); } @Override @@ -266,6 +330,12 @@ public void response(String key, Message message) { emit(NORTHBOUND_STREAM_ID, getCurrentTuple(), makeNorthboundTuple(key, message)); } + @Override + public void response(String key, InfoData payload) { + InfoMessage message = new InfoMessage(payload, System.currentTimeMillis(), key); + response(key, message); + } + @Override public void errorResponse(String key, ErrorType error, String message, String description) { ErrorData payload = new ErrorData(error, message, description); @@ -292,16 +362,24 @@ public void sendInactive() { @Override public void declareOutputFields(OutputFieldsDeclarer declarer) { super.declareOutputFields(declarer); - declarer.declareStream(SpeakerWorkerBolt.INCOME_STREAM, MessageKafkaTranslator.STREAM_FIELDS); + declarer.declareStream(SpeakerWorkerBolt.INCOME_STREAM, WORKER_STREAM_FIELDS); declarer.declareStream(NORTHBOUND_STREAM_ID, NORTHBOUND_STREAM_FIELDS); declarer.declareStream(ZOOKEEPER_STREAM_ID, ZOOKEEPER_STREAM_FIELDS); } - private Values makeWorkerTuple(String key, CommandData payload) { - return new Values(KeyProvider.generateChainedKey(key), payload, getCommandContext()); + private Values makeWorkerTuple(String key, CommandData payload, MessageCookie cookie) { + return new Values(KeyProvider.generateChainedKey(key), payload, cookie, getCommandContext()); } private Values makeNorthboundTuple(String key, Message payload) { return new Values(key, payload); } + + private static S registerService( + Map registry, String name, SwitchManagerCarrier targetCarrier, + Function provider) { + S service = provider.apply(new SwitchManagerCarrierCookieDecorator(targetCarrier, name)); + registry.put(new MessageCookie(name), service); + return service; + } } diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/error/OperationTimeoutException.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/error/OperationTimeoutException.java index c9011015a2a..2d55236df2e 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/error/OperationTimeoutException.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/error/OperationTimeoutException.java @@ -16,18 +16,9 @@ package org.openkilda.wfm.topology.switchmanager.error; import org.openkilda.messaging.error.ErrorType; -import org.openkilda.model.SwitchId; public class OperationTimeoutException extends SwitchManagerException { public OperationTimeoutException(String message) { super(ErrorType.OPERATION_TIMED_OUT, message); } - - public OperationTimeoutException(SwitchId switchId) { - super(ErrorType.OPERATION_TIMED_OUT, makeMessage(switchId)); - } - - private static String makeMessage(SwitchId switchId) { - return String.format("Switch %s validate/sync operation have timed out", switchId); - } } diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/fsm/SwitchSyncFsm.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/fsm/SwitchSyncFsm.java index f453483fe4d..b9923a2fe56 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/fsm/SwitchSyncFsm.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/fsm/SwitchSyncFsm.java @@ -249,6 +249,10 @@ SwitchSyncEvent, Object> builder() { .callMethod(FINISHED_WITH_ERROR_METHOD_NAME); builder.externalTransition().from(RULES_COMMANDS_SEND).to(FINISHED).on(NEXT) .callMethod(FINISHED_METHOD_NAME); + + builder.defineFinalState(FINISHED); + builder.defineFinalState(FINISHED_WITH_ERROR); + return builder; } @@ -750,8 +754,8 @@ private List mapToGroupEntryList(List groupIds, protected void finishedWithError(SwitchSyncState from, SwitchSyncState to, SwitchSyncEvent event, Object context) { - ErrorMessage sourceError = (ErrorMessage) context; - ErrorMessage message = new ErrorMessage(sourceError.getData(), System.currentTimeMillis(), key); + ErrorData payload = (ErrorData) context; + ErrorMessage message = new ErrorMessage(payload, System.currentTimeMillis(), key); log.error(ERROR_LOG_MESSAGE, key, message.getData().getErrorMessage()); @@ -762,8 +766,7 @@ protected void finishedWithError(SwitchSyncState from, SwitchSyncState to, private void sendException(Exception e) { ErrorData errorData = new SwitchSyncErrorData(switchId, ErrorType.INTERNAL_ERROR, e.getMessage(), "Error in SwitchSyncFsm"); - ErrorMessage errorMessage = new ErrorMessage(errorData, System.currentTimeMillis(), key); - fire(ERROR, errorMessage); + fire(ERROR, errorData); } public enum SwitchSyncState { diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/fsm/SwitchValidateFsm.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/fsm/SwitchValidateFsm.java index 65627235764..5515d56cdc6 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/fsm/SwitchValidateFsm.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/fsm/SwitchValidateFsm.java @@ -33,6 +33,7 @@ import org.openkilda.messaging.command.switches.DumpMetersForSwitchManagerRequest; import org.openkilda.messaging.command.switches.DumpRulesForSwitchManagerRequest; import org.openkilda.messaging.command.switches.SwitchValidateRequest; +import org.openkilda.messaging.error.ErrorType; import org.openkilda.messaging.info.InfoMessage; import org.openkilda.messaging.info.rule.FlowEntry; import org.openkilda.messaging.info.switches.SwitchValidationResponse; @@ -314,12 +315,21 @@ protected void finishedEnter(SwitchValidateState from, SwitchValidateState to, protected void finishedWithErrorEnter(SwitchValidateState from, SwitchValidateState to, SwitchValidateEvent event, SwitchValidateContext context) { - @SuppressWarnings("ThrowableNotThrown") + ErrorType type; + String description; SwitchManagerException error = context.getError(); - log.error("Switch {} (key: {}) validation failed - {}", getSwitchId(), key, error.getMessage()); + if (error != null) { + log.error("Switch {} (key: {}) validation failed - {}", getSwitchId(), key, error.getMessage()); + type = error.getError(); + description = error.getMessage(); + } else { + log.error("Switch {} (key: {}) validation failed - timeout", getSwitchId(), key); + type = ErrorType.OPERATION_TIMED_OUT; + description = String.format("Switch %s validate/sync operation have timed out", getSwitchId()); + } carrier.cancelTimeoutCallback(key); - carrier.errorResponse(key, error.getError(), error.getMessage(), "Error in switch validation"); + carrier.errorResponse(key, type, description, "Error in switch validation"); } // -- private/service methods -- diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/CreateLagPortService.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/CreateLagPortService.java index 1da5d01e51b..b1632dc2e58 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/CreateLagPortService.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/CreateLagPortService.java @@ -1,4 +1,4 @@ -/* Copyright 2021 Telstra Open Source +/* Copyright 2022 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. @@ -15,23 +15,136 @@ package org.openkilda.wfm.topology.switchmanager.service; -import org.openkilda.messaging.error.ErrorMessage; +import org.openkilda.messaging.MessageCookie; +import org.openkilda.messaging.error.ErrorData; +import org.openkilda.messaging.info.InfoData; import org.openkilda.messaging.info.grpc.CreateLogicalPortResponse; import org.openkilda.messaging.swmanager.request.CreateLagPortRequest; +import org.openkilda.wfm.error.MessageDispatchException; +import org.openkilda.wfm.error.UnexpectedInputException; +import org.openkilda.wfm.share.utils.FsmExecutor; +import org.openkilda.wfm.topology.switchmanager.error.OperationTimeoutException; +import org.openkilda.wfm.topology.switchmanager.error.SpeakerFailureException; +import org.openkilda.wfm.topology.switchmanager.fsm.CreateLagPortFsm; +import org.openkilda.wfm.topology.switchmanager.fsm.CreateLagPortFsm.CreateLagContext; +import org.openkilda.wfm.topology.switchmanager.fsm.CreateLagPortFsm.CreateLagEvent; +import org.openkilda.wfm.topology.switchmanager.fsm.CreateLagPortFsm.CreateLagState; -public interface CreateLagPortService { +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.squirrelframework.foundation.fsm.StateMachineBuilder; - void handleCreateLagRequest(String key, CreateLagPortRequest request); +import java.util.HashMap; +import java.util.Map; - void handleGrpcResponse(String key, CreateLogicalPortResponse response); +@Slf4j +public class CreateLagPortService implements SwitchManagerHubService { + @Getter + private final SwitchManagerCarrier carrier; - void handleTaskTimeout(String key); + private final LagPortOperationService lagPortOperationService; - void handleTaskError(String key, ErrorMessage message); + private final Map handlers = new HashMap<>(); + private final StateMachineBuilder builder; + private final FsmExecutor fsmExecutor; - void activate(); + private boolean active = true; - boolean deactivate(); + public CreateLagPortService(SwitchManagerCarrier carrier, LagPortOperationConfig config) { + this.lagPortOperationService = new LagPortOperationService(config); + this.builder = CreateLagPortFsm.builder(); + this.fsmExecutor = new FsmExecutor<>(CreateLagEvent.NEXT); + this.carrier = carrier; + } - boolean isAllOperationsCompleted(); + /** + * Handle LAG port request. + */ + public void handleCreateLagRequest(String key, CreateLagPortRequest request) { + CreateLagPortFsm fsm = builder.newStateMachine(CreateLagState.START, carrier, key, request, + lagPortOperationService); + handlers.put(key, fsm); + + fsm.start(); + fireFsmEvent(fsm, CreateLagEvent.NEXT, CreateLagContext.builder().build()); + } + + @Override + public void activate() { + active = true; + } + + @Override + public boolean deactivate() { + active = false; + return isAllOperationsCompleted(); + } + + @Override + public boolean isAllOperationsCompleted() { + return handlers.isEmpty(); + } + + @Override + public void timeout(@NonNull MessageCookie cookie) throws MessageDispatchException { + OperationTimeoutException error = new OperationTimeoutException("LAG create operation timeout"); + fireFsmEvent(cookie, CreateLagEvent.ERROR, CreateLagContext.builder().error(error).build()); + } + + @Override + public void dispatchWorkerMessage(InfoData payload, MessageCookie cookie) + throws UnexpectedInputException, MessageDispatchException { + if (payload instanceof CreateLogicalPortResponse) { + handleCreateOrUpdateResponse((CreateLogicalPortResponse) payload, cookie); + } else { + throw new UnexpectedInputException(payload); + } + } + + @Override + public void dispatchWorkerMessage(ErrorData payload, MessageCookie cookie) + throws MessageDispatchException { + CreateLagContext context = CreateLagContext.builder() + .error(new SpeakerFailureException(payload)) + .build(); + fireFsmEvent(cookie, CreateLagEvent.ERROR, context); + } + + private void handleCreateOrUpdateResponse(CreateLogicalPortResponse payload, MessageCookie cookie) + throws MessageDispatchException { + fireFsmEvent(cookie, CreateLagEvent.LAG_INSTALLED, + CreateLagContext.builder().createdLogicalPort(payload.getLogicalPort()).build()); + } + + private void fireFsmEvent(MessageCookie cookie, CreateLagEvent event, CreateLagContext context) + throws MessageDispatchException { + CreateLagPortFsm handler = null; + if (cookie != null) { + handler = handlers.get(cookie.getValue()); + } + if (handler == null) { + throw new MessageDispatchException(cookie); + } + fireFsmEvent(handler, event, context); + } + + private void fireFsmEvent(CreateLagPortFsm fsm, CreateLagEvent event, CreateLagContext context) { + fsmExecutor.fire(fsm, event, context); + removeIfCompleted(fsm); + } + + private void removeIfCompleted(CreateLagPortFsm fsm) { + if (fsm.isTerminated()) { + String requestKey = fsm.getKey(); + log.info("Create LAG {} FSM have reached termination state (key={})", fsm.getRequest(), requestKey); + + handlers.remove(requestKey); + carrier.cancelTimeoutCallback(requestKey); + + if (isAllOperationsCompleted() && !active) { + carrier.sendInactive(); + } + } + } } diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/DeleteLagPortService.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/DeleteLagPortService.java index 6667a6ee43c..24d4e1b0aa6 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/DeleteLagPortService.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/DeleteLagPortService.java @@ -1,4 +1,4 @@ -/* Copyright 2021 Telstra Open Source +/* Copyright 2022 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. @@ -15,23 +15,134 @@ package org.openkilda.wfm.topology.switchmanager.service; -import org.openkilda.messaging.error.ErrorMessage; +import org.openkilda.messaging.MessageCookie; +import org.openkilda.messaging.error.ErrorData; +import org.openkilda.messaging.info.InfoData; import org.openkilda.messaging.info.grpc.DeleteLogicalPortResponse; import org.openkilda.messaging.swmanager.request.DeleteLagPortRequest; +import org.openkilda.wfm.error.MessageDispatchException; +import org.openkilda.wfm.error.UnexpectedInputException; +import org.openkilda.wfm.share.utils.FsmExecutor; +import org.openkilda.wfm.topology.switchmanager.error.OperationTimeoutException; +import org.openkilda.wfm.topology.switchmanager.error.SpeakerFailureException; +import org.openkilda.wfm.topology.switchmanager.fsm.DeleteLagPortFsm; +import org.openkilda.wfm.topology.switchmanager.fsm.DeleteLagPortFsm.DeleteLagContext; +import org.openkilda.wfm.topology.switchmanager.fsm.DeleteLagPortFsm.DeleteLagEvent; +import org.openkilda.wfm.topology.switchmanager.fsm.DeleteLagPortFsm.DeleteLagState; -public interface DeleteLagPortService { +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.squirrelframework.foundation.fsm.StateMachineBuilder; - void handleDeleteLagRequest(String key, DeleteLagPortRequest request); +import java.util.HashMap; +import java.util.Map; - void handleGrpcResponse(String key, DeleteLogicalPortResponse response); +@Slf4j +public class DeleteLagPortService implements SwitchManagerHubService { + @Getter + private final SwitchManagerCarrier carrier; - void handleTaskTimeout(String key); + private final LagPortOperationService lagOperationService; - void handleTaskError(String key, ErrorMessage message); + private final Map handlers = new HashMap<>(); + private final StateMachineBuilder builder; + private final FsmExecutor fsmExecutor; - void activate(); + private boolean active = true; - boolean deactivate(); + public DeleteLagPortService(SwitchManagerCarrier carrier, LagPortOperationConfig config) { + this.lagOperationService = new LagPortOperationService(config); + this.builder = DeleteLagPortFsm.builder(); + this.fsmExecutor = new FsmExecutor<>(DeleteLagEvent.NEXT); + this.carrier = carrier; + } - boolean isAllOperationsCompleted(); + /** + * Handle delete LAG port request. + */ + public void handleDeleteLagRequest(String key, DeleteLagPortRequest request) { + DeleteLagPortFsm fsm = builder.newStateMachine( + DeleteLagState.START, carrier, key, request, lagOperationService); + handlers.put(key, fsm); + + fsm.start(); + fireFsmEvent(fsm, DeleteLagEvent.NEXT, DeleteLagContext.builder().build()); + } + + @Override + public void timeout(@NonNull MessageCookie cookie) throws MessageDispatchException { + OperationTimeoutException error = new OperationTimeoutException("LAG create operation timeout"); + fireFsmEvent(cookie, DeleteLagEvent.ERROR, DeleteLagContext.builder().error(error).build()); + } + + @Override + public void dispatchWorkerMessage(InfoData payload, MessageCookie cookie) + throws UnexpectedInputException, MessageDispatchException { + if (payload instanceof DeleteLogicalPortResponse) { + handleDeleteResponse((DeleteLogicalPortResponse) payload, cookie); + } else { + throw new UnexpectedInputException(payload); + } + } + + @Override + public void dispatchWorkerMessage(ErrorData payload, MessageCookie cookie) throws MessageDispatchException { + DeleteLagContext context = DeleteLagContext.builder() + .error(new SpeakerFailureException(payload)) + .build(); + fireFsmEvent(cookie, DeleteLagEvent.ERROR, context); + } + + private void handleDeleteResponse(DeleteLogicalPortResponse payload, MessageCookie cookie) + throws MessageDispatchException { + fireFsmEvent(cookie, DeleteLagEvent.LAG_REMOVED, + DeleteLagContext.builder().deletedLogicalPort(payload.getLogicalPortNumber()).build()); + } + + @Override + public void activate() { + active = true; + } + + @Override + public boolean deactivate() { + active = false; + return isAllOperationsCompleted(); + } + + @Override + public boolean isAllOperationsCompleted() { + return handlers.isEmpty(); + } + + private void fireFsmEvent(MessageCookie cookie, DeleteLagEvent event, DeleteLagContext context) + throws MessageDispatchException { + DeleteLagPortFsm handler = null; + if (cookie != null) { + handler = handlers.get(cookie.getValue()); + } + if (handler == null) { + throw new MessageDispatchException(cookie); + } + fireFsmEvent(handler, event, context); + } + + private void fireFsmEvent(DeleteLagPortFsm fsm, DeleteLagEvent event, DeleteLagContext context) { + fsmExecutor.fire(fsm, event, context); + removeIfCompleted(fsm); + } + + private void removeIfCompleted(DeleteLagPortFsm fsm) { + if (fsm.isTerminated()) { + String requestKey = fsm.getKey(); + log.info("Delete LAG {} FSM have reached termination state (key={})", fsm.getRequest(), requestKey); + handlers.remove(requestKey); + carrier.cancelTimeoutCallback(requestKey); + + if (isAllOperationsCompleted() && !active) { + carrier.sendInactive(); + } + } + } } diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchManagerCarrier.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchManagerCarrier.java index 61d71aaf4c0..3e76283a4fc 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchManagerCarrier.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchManagerCarrier.java @@ -16,16 +16,26 @@ package org.openkilda.wfm.topology.switchmanager.service; import org.openkilda.messaging.Message; +import org.openkilda.messaging.MessageCookie; import org.openkilda.messaging.command.CommandData; import org.openkilda.messaging.command.switches.SwitchValidateRequest; import org.openkilda.messaging.error.ErrorType; +import org.openkilda.messaging.info.InfoData; import org.openkilda.wfm.topology.switchmanager.model.ValidationResult; +import lombok.NonNull; + public interface SwitchManagerCarrier { + MessageCookie newDispatchRoute(String requestKey); + void sendCommandToSpeaker(String key, CommandData command); + void sendCommandToSpeaker(CommandData command, @NonNull MessageCookie cookie); + void response(String key, Message message); + void response(String key, InfoData payload); + void errorResponse(String key, ErrorType error, String message, String description); void cancelTimeoutCallback(String key); diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchManagerCarrierCookieDecorator.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchManagerCarrierCookieDecorator.java new file mode 100644 index 00000000000..ed086ac6033 --- /dev/null +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchManagerCarrierCookieDecorator.java @@ -0,0 +1,82 @@ +/* Copyright 2022 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.switchmanager.service; + +import org.openkilda.messaging.Message; +import org.openkilda.messaging.MessageCookie; +import org.openkilda.messaging.command.CommandData; +import org.openkilda.messaging.command.switches.SwitchValidateRequest; +import org.openkilda.messaging.error.ErrorType; +import org.openkilda.messaging.info.InfoData; +import org.openkilda.wfm.topology.switchmanager.model.ValidationResult; + +import lombok.NonNull; + +public class SwitchManagerCarrierCookieDecorator implements SwitchManagerCarrier { + private final SwitchManagerCarrier target; + + private final String dispatchRoute; + + public SwitchManagerCarrierCookieDecorator(SwitchManagerCarrier target, String dispatchRoute) { + this.target = target; + this.dispatchRoute = dispatchRoute; + } + + @Override + public MessageCookie newDispatchRoute(String requestKey) { + return new MessageCookie(dispatchRoute, target.newDispatchRoute(requestKey)); + } + + @Override + public void sendCommandToSpeaker(String key, CommandData command) { + sendCommandToSpeaker(command, new MessageCookie(key)); + } + + @Override + public void sendCommandToSpeaker(CommandData command, @NonNull MessageCookie cookie) { + target.sendCommandToSpeaker(command, new MessageCookie(dispatchRoute, cookie)); + } + + @Override + public void response(String key, Message message) { + target.response(key, message); + } + + @Override + public void response(String key, InfoData payload) { + target.response(key, payload); + } + + @Override + public void errorResponse(String key, ErrorType error, String message, String description) { + target.errorResponse(key, error, message, description); + } + + @Override + public void cancelTimeoutCallback(String key) { + target.cancelTimeoutCallback(key); + } + + @Override + public void runSwitchSync(String key, SwitchValidateRequest request, ValidationResult validationResult) { + target.runSwitchSync(key, request, validationResult); + } + + @Override + public void sendInactive() { + target.sendInactive(); + } +} diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchManagerHubService.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchManagerHubService.java new file mode 100644 index 00000000000..64b71a68eb7 --- /dev/null +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchManagerHubService.java @@ -0,0 +1,41 @@ +/* Copyright 2022 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.switchmanager.service; + +import org.openkilda.messaging.MessageCookie; +import org.openkilda.messaging.error.ErrorData; +import org.openkilda.messaging.info.InfoData; +import org.openkilda.wfm.error.MessageDispatchException; +import org.openkilda.wfm.error.UnexpectedInputException; + +import lombok.NonNull; + +public interface SwitchManagerHubService { + void activate(); + + boolean deactivate(); + + boolean isAllOperationsCompleted(); + + void timeout(@NonNull MessageCookie cookie) throws MessageDispatchException; + + void dispatchWorkerMessage(InfoData payload, MessageCookie cookie) + throws UnexpectedInputException, MessageDispatchException; + + void dispatchWorkerMessage(ErrorData payload, MessageCookie cookie) throws MessageDispatchException; + + SwitchManagerCarrier getCarrier(); +} diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchRuleService.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchRuleService.java index 76a7661bf18..98b5981e13f 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchRuleService.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchRuleService.java @@ -1,4 +1,4 @@ -/* Copyright 2019 Telstra Open Source +/* Copyright 2022 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. @@ -15,36 +15,247 @@ package org.openkilda.wfm.topology.switchmanager.service; +import static java.lang.String.format; + +import org.openkilda.messaging.MessageCookie; import org.openkilda.messaging.command.switches.SwitchRulesDeleteRequest; import org.openkilda.messaging.command.switches.SwitchRulesInstallRequest; +import org.openkilda.messaging.error.ErrorData; +import org.openkilda.messaging.error.ErrorMessage; +import org.openkilda.messaging.error.ErrorType; +import org.openkilda.messaging.info.InfoData; +import org.openkilda.messaging.info.InfoMessage; import org.openkilda.messaging.info.switches.SwitchRulesResponse; +import org.openkilda.model.FlowPath; +import org.openkilda.model.KildaFeatureToggles; +import org.openkilda.model.SwitchId; +import org.openkilda.model.SwitchProperties; +import org.openkilda.persistence.repositories.FlowPathRepository; +import org.openkilda.persistence.repositories.IslRepository; +import org.openkilda.persistence.repositories.KildaFeatureTogglesRepository; +import org.openkilda.persistence.repositories.RepositoryFactory; +import org.openkilda.persistence.repositories.SwitchPropertiesRepository; +import org.openkilda.persistence.repositories.SwitchRepository; +import org.openkilda.wfm.error.MessageDispatchException; -public interface SwitchRuleService { +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; - /** - * handles remove rule request. - * @param key key - * @param data request payload - */ - void deleteRules(String key, SwitchRulesDeleteRequest data); +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +public class SwitchRuleService implements SwitchManagerHubService { + @Getter + private SwitchManagerCarrier carrier; + + private FlowPathRepository flowPathRepository; + private SwitchPropertiesRepository switchPropertiesRepository; + private KildaFeatureTogglesRepository featureTogglesRepository; + private IslRepository islRepository; + private SwitchRepository switchRepository; + + private boolean active = true; + + private boolean isOperationCompleted = true; + + public SwitchRuleService(SwitchManagerCarrier carrier, RepositoryFactory repositoryFactory) { + flowPathRepository = repositoryFactory.createFlowPathRepository(); + switchPropertiesRepository = repositoryFactory.createSwitchPropertiesRepository(); + featureTogglesRepository = repositoryFactory.createFeatureTogglesRepository(); + islRepository = repositoryFactory.createIslRepository(); + switchRepository = repositoryFactory.createSwitchRepository(); + this.carrier = carrier; + } + + @Override + public void timeout(@NonNull MessageCookie cookie) { + log.info("Got timeout notification for request key \"{}\"", cookie.getValue()); + } + + @Override + public void dispatchWorkerMessage(InfoData payload, MessageCookie cookie) throws MessageDispatchException { + if (payload instanceof SwitchRulesResponse) { + rulesResponse(cookie.getValue(), (SwitchRulesResponse) payload); + } else { + throw new MessageDispatchException(cookie); + } + } + + @Override + public void dispatchWorkerMessage(ErrorData payload, MessageCookie cookie) { + // FIXME(surabujin): the service completely ignores error responses + log.error("Got speaker error response: {} (request key: {})", payload, cookie.getValue()); + } /** - * handles rule response. - * @param key key - * @param response payload + * Handle delete rules request. */ - void rulesResponse(String key, SwitchRulesResponse response); + public void deleteRules(String key, SwitchRulesDeleteRequest data) { + isOperationCompleted = false; // FIXME(surabujin): what it supposed to do? Can we get rid of it? + SwitchId switchId = data.getSwitchId(); + if (!switchRepository.exists(switchId)) { + ErrorData errorData = new ErrorData(ErrorType.NOT_FOUND, format("Switch %s not found", switchId), + "Error when deleting switch rules"); + ErrorMessage errorMessage = new ErrorMessage(errorData, System.currentTimeMillis(), key); + + carrier.response(key, errorMessage); + return; + } + Optional switchProperties = switchPropertiesRepository.findBySwitchId(switchId); + KildaFeatureToggles featureToggles = featureTogglesRepository.getOrDefault(); + boolean server42FlowRttFeatureToggle = featureToggles.getServer42FlowRtt(); + data.setServer42FlowRttFeatureToggle(server42FlowRttFeatureToggle); + data.setServer42IslRttEnabled(featureToggles.getServer42IslRtt() + && switchProperties.map(SwitchProperties::hasServer42IslRttEnabled).orElse(false)); + + if (switchProperties.isPresent()) { + data.setMultiTable(switchProperties.get().isMultiTable()); + data.setSwitchLldp(switchProperties.get().isSwitchLldp()); + data.setSwitchArp(switchProperties.get().isSwitchArp()); + data.setServer42FlowRttSwitchProperty(switchProperties.get().isServer42FlowRtt()); + data.setServer42Port(switchProperties.get().getServer42Port()); + data.setServer42Vlan(switchProperties.get().getServer42Vlan()); + data.setServer42MacAddress(switchProperties.get().getServer42MacAddress()); + Collection flowPaths = flowPathRepository.findBySrcSwitch(switchId); + List flowPorts = new ArrayList<>(); + Set flowLldpPorts = new HashSet<>(); + Set flowArpPorts = new HashSet<>(); + Set server42FlowPorts = new HashSet<>(); + fillFlowPorts(switchProperties.get(), flowPaths, flowPorts, flowLldpPorts, flowArpPorts, server42FlowPorts, + server42FlowRttFeatureToggle && switchProperties.get().isServer42FlowRtt()); + + data.setFlowPorts(flowPorts); + data.setFlowLldpPorts(flowLldpPorts); + data.setFlowArpPorts(flowArpPorts); + data.setServer42FlowRttPorts(server42FlowPorts); + List islPorts = islRepository.findBySrcSwitch(switchId).stream() + .map(isl -> isl.getSrcPort()) + .collect(Collectors.toList()); + data.setIslPorts(islPorts); + } + carrier.sendCommandToSpeaker(key, data); + } /** - * handles install rule request. - * @param key key - * @param data request payload + * Handle install rules request. */ - void installRules(String key, SwitchRulesInstallRequest data); + public void installRules(String key, SwitchRulesInstallRequest data) { + isOperationCompleted = false; // FIXME(surabujin): what it supposed to do? Can we get rid of it? + SwitchId switchId = data.getSwitchId(); + if (!switchRepository.exists(switchId)) { + ErrorData errorData = new ErrorData(ErrorType.NOT_FOUND, format("Switch %s not found", switchId), + "Error when installing switch rules"); + ErrorMessage errorMessage = new ErrorMessage(errorData, System.currentTimeMillis(), key); + + carrier.response(key, errorMessage); + return; + } + Optional switchProperties = switchPropertiesRepository.findBySwitchId(switchId); + + KildaFeatureToggles featureToggles = featureTogglesRepository.getOrDefault(); + boolean server42FlowRttFeatureToggle = featureToggles.getServer42FlowRtt(); + data.setServer42FlowRttFeatureToggle(server42FlowRttFeatureToggle); + data.setServer42IslRttEnabled(featureToggles.getServer42IslRtt() + && switchProperties.map(SwitchProperties::hasServer42IslRttEnabled).orElse(false)); + + if (switchProperties.isPresent()) { + data.setMultiTable(switchProperties.get().isMultiTable()); + data.setSwitchLldp(switchProperties.get().isSwitchLldp()); + data.setSwitchArp(switchProperties.get().isSwitchArp()); + data.setServer42FlowRttSwitchProperty(switchProperties.get().isServer42FlowRtt()); + data.setServer42Port(switchProperties.get().getServer42Port()); + data.setServer42Vlan(switchProperties.get().getServer42Vlan()); + data.setServer42MacAddress(switchProperties.get().getServer42MacAddress()); + Collection flowPaths = flowPathRepository.findBySrcSwitch(switchId); + List flowPorts = new ArrayList<>(); + Set flowLldpPorts = new HashSet<>(); + Set flowArpPorts = new HashSet<>(); + Set server42FlowPorts = new HashSet<>(); + fillFlowPorts(switchProperties.get(), flowPaths, flowPorts, flowLldpPorts, flowArpPorts, server42FlowPorts, + server42FlowRttFeatureToggle && switchProperties.get().isServer42FlowRtt()); + data.setFlowPorts(flowPorts); + data.setFlowLldpPorts(flowLldpPorts); + data.setFlowArpPorts(flowArpPorts); + data.setServer42FlowRttPorts(server42FlowPorts); + List islPorts = islRepository.findBySrcSwitch(switchId).stream() + .map(isl -> isl.getSrcPort()) + .collect(Collectors.toList()); + data.setIslPorts(islPorts); + } + carrier.sendCommandToSpeaker(key, data); + } + + private void fillFlowPorts(SwitchProperties switchProperties, Collection flowPaths, + List flowPorts, Set flowLldpPorts, Set flowArpPorts, + Set server42FlowPorts, boolean server42Rtt) { + for (FlowPath flowPath : flowPaths) { + if (flowPath.isForward()) { + if (flowPath.isSrcWithMultiTable()) { + flowPorts.add(flowPath.getFlow().getSrcPort()); + if (server42Rtt && !flowPath.getFlow().isOneSwitchFlow()) { + server42FlowPorts.add(flowPath.getFlow().getSrcPort()); + } + } + if (flowPath.getFlow().getDetectConnectedDevices().isSrcLldp() + || switchProperties.isSwitchLldp()) { + flowLldpPorts.add(flowPath.getFlow().getSrcPort()); + } + if (flowPath.getFlow().getDetectConnectedDevices().isSrcArp() + || switchProperties.isSwitchArp()) { + flowArpPorts.add(flowPath.getFlow().getSrcPort()); + } + } else { + if (flowPath.isDestWithMultiTable()) { + flowPorts.add(flowPath.getFlow().getDestPort()); + if (server42Rtt && !flowPath.getFlow().isOneSwitchFlow()) { + server42FlowPorts.add(flowPath.getFlow().getDestPort()); + } + } + if (flowPath.getFlow().getDetectConnectedDevices().isDstLldp() + || switchProperties.isSwitchLldp()) { + flowLldpPorts.add(flowPath.getFlow().getDestPort()); + } + if (flowPath.getFlow().getDetectConnectedDevices().isDstArp() + || switchProperties.isSwitchArp()) { + flowArpPorts.add(flowPath.getFlow().getDestPort()); + } + } + } + } + + private void rulesResponse(String key, SwitchRulesResponse response) { + carrier.cancelTimeoutCallback(key); + InfoMessage message = new InfoMessage(response, System.currentTimeMillis(), key); + + carrier.response(key, message); + + isOperationCompleted = true; // FIXME(surabujin): what it supposed to do? Can we get rid of it? + + if (!active) { + carrier.sendInactive(); + } + } - void activate(); + @Override + public void activate() { + active = true; + } - boolean deactivate(); + @Override + public boolean deactivate() { + active = false; + return isAllOperationsCompleted(); + } - boolean isAllOperationsCompleted(); + @Override + public boolean isAllOperationsCompleted() { + return isOperationCompleted; + } } diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchSyncService.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchSyncService.java index 1efdab644bb..2742ab2922b 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchSyncService.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchSyncService.java @@ -1,4 +1,4 @@ -/* Copyright 2019 Telstra Open Source +/* Copyright 2022 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. @@ -15,42 +15,171 @@ package org.openkilda.wfm.topology.switchmanager.service; +import org.openkilda.messaging.MessageCookie; import org.openkilda.messaging.command.switches.SwitchValidateRequest; -import org.openkilda.messaging.error.ErrorMessage; +import org.openkilda.messaging.error.ErrorData; +import org.openkilda.messaging.info.InfoData; +import org.openkilda.messaging.info.flow.FlowInstallResponse; import org.openkilda.messaging.info.flow.FlowReinstallResponse; +import org.openkilda.messaging.info.flow.FlowRemoveResponse; +import org.openkilda.messaging.info.grpc.CreateLogicalPortResponse; +import org.openkilda.messaging.info.grpc.DeleteLogicalPortResponse; +import org.openkilda.messaging.info.switches.DeleteGroupResponse; +import org.openkilda.messaging.info.switches.DeleteMeterResponse; +import org.openkilda.messaging.info.switches.InstallGroupResponse; +import org.openkilda.messaging.info.switches.ModifyGroupResponse; +import org.openkilda.messaging.info.switches.ModifyMeterResponse; +import org.openkilda.persistence.PersistenceManager; +import org.openkilda.wfm.error.MessageDispatchException; +import org.openkilda.wfm.error.UnexpectedInputException; +import org.openkilda.wfm.share.flow.resources.FlowResourcesConfig; +import org.openkilda.wfm.topology.switchmanager.fsm.SwitchSyncFsm; +import org.openkilda.wfm.topology.switchmanager.fsm.SwitchSyncFsm.SwitchSyncEvent; +import org.openkilda.wfm.topology.switchmanager.fsm.SwitchSyncFsm.SwitchSyncState; import org.openkilda.wfm.topology.switchmanager.model.ValidationResult; - -public interface SwitchSyncService { - - void handleSwitchSync(String key, SwitchValidateRequest request, ValidationResult validationResult); - - void handleInstallRulesResponse(String key); - - void handleRemoveRulesResponse(String key); - - void handleReinstallDefaultRulesResponse(String key, FlowReinstallResponse response); - - void handleRemoveMetersResponse(String key); - - void handleModifyMetersResponse(String key); - - void handleInstallGroupResponse(String key); - - void handleModifyGroupResponse(String key); - - void handleDeleteGroupResponse(String key); - - void handleCreateLogicalPortResponse(String key); - - void handleDeleteLogicalPortResponse(String key); - - void handleTaskTimeout(String key); - - void handleTaskError(String key, ErrorMessage message); - - void activate(); - - boolean deactivate(); - - boolean isAllOperationsCompleted(); +import org.openkilda.wfm.topology.switchmanager.service.impl.CommandBuilderImpl; + +import com.google.common.annotations.VisibleForTesting; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.squirrelframework.foundation.fsm.StateMachineBuilder; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +public class SwitchSyncService implements SwitchManagerHubService { + @Getter + private SwitchManagerCarrier carrier; + + private Map handlers = new HashMap<>(); + private StateMachineBuilder builder; + + @Getter + private boolean active = true; + + @VisibleForTesting + CommandBuilder commandBuilder; + + public SwitchSyncService( + SwitchManagerCarrier carrier, PersistenceManager persistenceManager, + FlowResourcesConfig flowResourcesConfig) { + this(carrier, new CommandBuilderImpl(persistenceManager, flowResourcesConfig)); + } + + @VisibleForTesting + SwitchSyncService(SwitchManagerCarrier carrier, CommandBuilder commandBuilder) { + this.carrier = carrier; + this.commandBuilder = commandBuilder; + this.builder = SwitchSyncFsm.builder(); + } + + @Override + public void timeout(@NonNull MessageCookie cookie) throws MessageDispatchException { + fireHandlerEvent(cookie, SwitchSyncEvent.TIMEOUT); + } + + @Override + public void dispatchWorkerMessage(InfoData payload, MessageCookie cookie) + throws UnexpectedInputException, MessageDispatchException { + if (payload instanceof FlowInstallResponse) { + fireHandlerEvent(cookie, SwitchSyncEvent.MISSING_RULES_INSTALLED); + } else if (payload instanceof FlowRemoveResponse) { + fireHandlerEvent(cookie, SwitchSyncEvent.EXCESS_RULES_REMOVED); + } else if (payload instanceof FlowReinstallResponse) { + fireHandlerEvent(cookie, SwitchSyncEvent.MISCONFIGURED_RULES_REINSTALLED, payload); + } else if (payload instanceof DeleteMeterResponse) { + fireHandlerEvent(cookie, SwitchSyncEvent.METERS_REMOVED); + } else if (payload instanceof ModifyMeterResponse) { + fireHandlerEvent(cookie, SwitchSyncEvent.MISCONFIGURED_METERS_MODIFIED); + } else if (payload instanceof InstallGroupResponse) { + fireHandlerEvent(cookie, SwitchSyncEvent.GROUPS_INSTALLED); + } else if (payload instanceof ModifyGroupResponse) { + fireHandlerEvent(cookie, SwitchSyncEvent.GROUPS_MODIFIED); + } else if (payload instanceof DeleteGroupResponse) { + fireHandlerEvent(cookie, SwitchSyncEvent.GROUPS_REMOVED); + } else if (payload instanceof CreateLogicalPortResponse) { + fireHandlerEvent(cookie, SwitchSyncEvent.LOGICAL_PORT_INSTALLED); + } else if (payload instanceof DeleteLogicalPortResponse) { + fireHandlerEvent(cookie, SwitchSyncEvent.LOGICAL_PORT_REMOVED); + } else { + throw new UnexpectedInputException(payload); + } + } + + @Override + public void dispatchWorkerMessage(ErrorData payload, MessageCookie cookie) throws MessageDispatchException { + fireHandlerEvent(cookie, SwitchSyncEvent.ERROR, payload); + } + + /** + * Handle switch sync request. + */ + public void handleSwitchSync(String key, SwitchValidateRequest request, ValidationResult validationResult) { + SwitchSyncFsm fsm = + builder.newStateMachine(SwitchSyncState.INITIALIZED, carrier, key, commandBuilder, request, + validationResult); + handlers.put(key, fsm); + process(fsm); + } + + private void fireHandlerEvent(MessageCookie cookie, SwitchSyncEvent event) throws MessageDispatchException { + fireHandlerEvent(cookie, event, null); + } + + private void fireHandlerEvent(MessageCookie cookie, SwitchSyncEvent event, Object context) + throws MessageDispatchException { + SwitchSyncFsm handler = null; + if (cookie != null) { + handler = handlers.get(cookie.getValue()); + } + if (handler == null) { + throw new MessageDispatchException(cookie); + } + + handler.fire(event, context); + process(handler); + } + + // FIXME(surabujin): incorrect FSM usage + private void process(SwitchSyncFsm fsm) { + final List stopStates = Arrays.asList( + SwitchSyncState.RULES_COMMANDS_SEND, + SwitchSyncState.METERS_COMMANDS_SEND, + SwitchSyncState.GROUPS_COMMANDS_SEND, + SwitchSyncState.LOGICAL_PORTS_COMMANDS_SEND, + SwitchSyncState.FINISHED, + SwitchSyncState.FINISHED_WITH_ERROR + ); + + while (!stopStates.contains(fsm.getCurrentState())) { + fsm.fire(SwitchSyncEvent.NEXT); + } + + if (fsm.isTerminated()) { + handlers.remove(fsm.getKey()); + if (isAllOperationsCompleted() && !active) { + carrier.sendInactive(); + } + } + } + + @Override + public void activate() { + active = true; + } + + @Override + public boolean deactivate() { + active = false; + return isAllOperationsCompleted(); + } + + @Override + public boolean isAllOperationsCompleted() { + return handlers.isEmpty(); + } } diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchValidateService.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchValidateService.java index fefa8edb830..b5167c4c19f 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchValidateService.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchValidateService.java @@ -15,34 +15,197 @@ package org.openkilda.wfm.topology.switchmanager.service; +import org.openkilda.messaging.MessageCookie; import org.openkilda.messaging.command.switches.SwitchValidateRequest; -import org.openkilda.messaging.error.ErrorMessage; +import org.openkilda.messaging.error.ErrorData; +import org.openkilda.messaging.info.InfoData; import org.openkilda.messaging.info.flow.FlowDumpResponse; import org.openkilda.messaging.info.group.GroupDumpResponse; import org.openkilda.messaging.info.grpc.DumpLogicalPortsResponse; import org.openkilda.messaging.info.meter.MeterDumpResponse; +import org.openkilda.messaging.info.meter.SwitchMeterUnsupported; +import org.openkilda.persistence.PersistenceManager; +import org.openkilda.rulemanager.RuleManager; +import org.openkilda.wfm.error.MessageDispatchException; +import org.openkilda.wfm.error.UnexpectedInputException; +import org.openkilda.wfm.share.metrics.MeterRegistryHolder; +import org.openkilda.wfm.share.utils.FsmExecutor; +import org.openkilda.wfm.topology.switchmanager.error.SpeakerFailureException; +import org.openkilda.wfm.topology.switchmanager.fsm.SwitchValidateFsm; +import org.openkilda.wfm.topology.switchmanager.fsm.SwitchValidateFsm.SwitchValidateContext; +import org.openkilda.wfm.topology.switchmanager.fsm.SwitchValidateFsm.SwitchValidateEvent; +import org.openkilda.wfm.topology.switchmanager.fsm.SwitchValidateFsm.SwitchValidateState; -public interface SwitchValidateService { +import io.micrometer.core.instrument.LongTaskTimer; +import io.micrometer.core.instrument.LongTaskTimer.Sample; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.squirrelframework.foundation.fsm.StateMachineBuilder; - void handleSwitchValidateRequest(String key, SwitchValidateRequest data); +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; - void handleFlowEntriesResponse(String key, FlowDumpResponse data); +@Slf4j +public class SwitchValidateService implements SwitchManagerHubService { + @Getter + private final SwitchManagerCarrier carrier; - void handleGroupEntriesResponse(String key, GroupDumpResponse data); + private final Map handlers = new HashMap<>(); - void handleLogicalPortResponse(String key, DumpLogicalPortsResponse data); + private final ValidationService validationService; + private final RuleManager ruleManager; + private final StateMachineBuilder< + SwitchValidateFsm, SwitchValidateState, SwitchValidateEvent, SwitchValidateContext> builder; + private final FsmExecutor< + SwitchValidateFsm, SwitchValidateState, SwitchValidateEvent, SwitchValidateContext> fsmExecutor; - void handleMeterEntriesResponse(String key, MeterDumpResponse data); + private final PersistenceManager persistenceManager; - void handleMetersUnsupportedResponse(String key); + @Getter + private boolean active = true; - void handleTaskTimeout(String key); + public SwitchValidateService( + SwitchManagerCarrier carrier, PersistenceManager persistenceManager, ValidationService validationService, + RuleManager ruleManager) { + this.carrier = carrier; + this.builder = SwitchValidateFsm.builder(); + this.fsmExecutor = new FsmExecutor<>(SwitchValidateEvent.NEXT); + this.validationService = validationService; + this.ruleManager = ruleManager; + this.persistenceManager = persistenceManager; + } - void handleTaskError(String key, ErrorMessage message); + @Override + public void timeout(@NonNull MessageCookie cookie) throws MessageDispatchException { + handle(cookie, SwitchValidateEvent.ERROR, SwitchValidateContext.builder().build()); + } - void activate(); + @Override + public void dispatchWorkerMessage(InfoData payload, MessageCookie cookie) + throws UnexpectedInputException, MessageDispatchException { + if (payload instanceof FlowDumpResponse) { + handleFlowEntriesResponse((FlowDumpResponse) payload, cookie); + } else if (payload instanceof GroupDumpResponse) { + handleGroupEntriesResponse((GroupDumpResponse) payload, cookie); + } else if (payload instanceof DumpLogicalPortsResponse) { + handleLogicalPortResponse((DumpLogicalPortsResponse) payload, cookie); + } else if (payload instanceof MeterDumpResponse) { + handleMeterEntriesResponse((MeterDumpResponse) payload, cookie); + } else if (payload instanceof SwitchMeterUnsupported) { + handleMetersUnsupportedResponse(cookie); + } else { + throw new UnexpectedInputException(payload); + } + } - boolean deactivate(); + @Override + public void dispatchWorkerMessage(ErrorData payload, MessageCookie cookie) throws MessageDispatchException { + SwitchValidateContext context = SwitchValidateContext.builder() + .error(new SpeakerFailureException(payload)) + .build(); + handle(cookie, SwitchValidateEvent.ERROR, context); + } - boolean isAllOperationsCompleted(); + /** + * Handle switch validate request. + */ + public void handleSwitchValidateRequest(String key, SwitchValidateRequest request) { + SwitchValidateFsm fsm = + builder.newStateMachine( + SwitchValidateState.START, carrier, key, request, validationService, ruleManager, + persistenceManager); + MeterRegistryHolder.getRegistry().ifPresent(registry -> { + Sample sample = LongTaskTimer.builder("fsm.active_execution") + .register(registry) + .start(); + fsm.addTerminateListener(e -> { + long duration = sample.stop(); + if (fsm.getCurrentState() == SwitchValidateState.FINISHED) { + registry.timer("fsm.execution.success") + .record(duration, TimeUnit.NANOSECONDS); + } else if (fsm.getCurrentState() == SwitchValidateState.FINISHED_WITH_ERROR) { + registry.timer("fsm.execution.failed") + .record(duration, TimeUnit.NANOSECONDS); + } + }); + }); + handlers.put(key, fsm); + + fsm.start(); + handle(fsm, SwitchValidateEvent.NEXT, SwitchValidateContext.builder().build()); + } + + private void handleFlowEntriesResponse(FlowDumpResponse data, MessageCookie cookie) + throws MessageDispatchException { + handle(cookie, SwitchValidateEvent.RULES_RECEIVED, + SwitchValidateContext.builder().flowEntries(data.getFlowSpeakerData()).build()); + } + + private void handleGroupEntriesResponse(GroupDumpResponse data, MessageCookie cookie) + throws MessageDispatchException { + handle(cookie, SwitchValidateEvent.GROUPS_RECEIVED, + SwitchValidateContext.builder().groupEntries(data.getGroupSpeakerData()).build()); + } + + private void handleLogicalPortResponse(DumpLogicalPortsResponse data, MessageCookie cookie) + throws MessageDispatchException { + handle(cookie, SwitchValidateEvent.LOGICAL_PORTS_RECEIVED, SwitchValidateContext.builder() + .logicalPortEntries(data.getLogicalPorts()).build()); + } + + private void handleMeterEntriesResponse(MeterDumpResponse data, MessageCookie cookie) + throws MessageDispatchException { + handle(cookie, SwitchValidateEvent.METERS_RECEIVED, + SwitchValidateContext.builder().meterEntries(data.getMeterSpeakerData()).build()); + } + + private void handleMetersUnsupportedResponse(MessageCookie key) throws MessageDispatchException { + handle(key, SwitchValidateEvent.METERS_UNSUPPORTED, SwitchValidateContext.builder().build()); + } + + private void handle(MessageCookie cookie, SwitchValidateEvent event, SwitchValidateContext context) + throws MessageDispatchException { + SwitchValidateFsm handler = null; + if (cookie != null) { + handler = handlers.get(cookie.getValue()); + } + if (handler == null) { + throw new MessageDispatchException(cookie); + } + handle(handler, event, context); + } + + private void handle(SwitchValidateFsm fsm, SwitchValidateEvent event, SwitchValidateContext context) { + fsmExecutor.fire(fsm, event, context); + removeIfCompleted(fsm); + } + + void removeIfCompleted(SwitchValidateFsm fsm) { + if (fsm.isTerminated()) { + log.info("Switch {} validation FSM have reached termination state (key={})", + fsm.getSwitchId(), fsm.getKey()); + handlers.remove(fsm.getKey()); + if (isAllOperationsCompleted() && !active) { + carrier.sendInactive(); + } + } + } + + @Override + public void activate() { + active = true; + } + + @Override + public boolean deactivate() { + active = false; + return isAllOperationsCompleted(); + } + + @Override + public boolean isAllOperationsCompleted() { + return handlers.isEmpty(); + } } diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/SpeakerWorkerService.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/SpeakerWorkerService.java index 300413dde50..52ffb281546 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/SpeakerWorkerService.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/SpeakerWorkerService.java @@ -16,6 +16,7 @@ package org.openkilda.wfm.topology.switchmanager.service.impl; import org.openkilda.messaging.Message; +import org.openkilda.messaging.MessageCookie; import org.openkilda.messaging.command.CommandData; import org.openkilda.messaging.command.CommandMessage; import org.openkilda.messaging.error.ErrorData; @@ -24,6 +25,7 @@ import org.openkilda.wfm.error.PipelineException; import org.openkilda.wfm.topology.switchmanager.service.SpeakerCommandCarrier; +import lombok.Value; import lombok.extern.slf4j.Slf4j; import java.util.HashMap; @@ -33,7 +35,7 @@ public class SpeakerWorkerService { private final SpeakerCommandCarrier carrier; - private final Map keyToRequest = new HashMap<>(); + private final Map keyToRequest = new HashMap<>(); public SpeakerWorkerService(SpeakerCommandCarrier carrier) { this.carrier = carrier; @@ -44,9 +46,9 @@ public SpeakerWorkerService(SpeakerCommandCarrier carrier) { * @param key unique operation's key. * @param command command to be executed. */ - public void sendFloodlightCommand(String key, CommandData command) throws PipelineException { + public void sendFloodlightCommand(String key, CommandData command, MessageCookie cookie) throws PipelineException { log.debug("Got Floodlight request from hub bolt {}", command); - keyToRequest.put(key, command); + keyToRequest.put(key, new RequestContext(command, cookie)); carrier.sendFloodlightCommand(key, new CommandMessage(command, System.currentTimeMillis(), key)); } @@ -55,10 +57,10 @@ public void sendFloodlightCommand(String key, CommandData command) throws Pipeli * @param key unique operation's key. * @param command command to be executed. */ - public void sendGrpcCommand(String key, CommandData command) throws PipelineException { + public void sendGrpcCommand(String key, CommandData command, MessageCookie cookie) throws PipelineException { log.debug("Got GRPC request from hub bolt {}", command); - keyToRequest.put(key, command); - carrier.sendGrpcCommand(key, new CommandMessage(command, System.currentTimeMillis(), key)); + keyToRequest.put(key, new RequestContext(command, cookie)); + carrier.sendGrpcCommand(key, new CommandMessage(command, key, cookie)); } /** @@ -69,8 +71,11 @@ public void sendGrpcCommand(String key, CommandData command) throws PipelineExce public void handleResponse(String key, Message response) throws PipelineException { log.debug("Got a response from speaker {}", response); - CommandData pendingRequest = keyToRequest.remove(key); - if (pendingRequest != null) { + RequestContext pending = keyToRequest.remove(key); + if (pending != null) { + if (response.getCookie() == null) { + response.setCookie(pending.getCookie()); + } carrier.sendResponse(key, response); } } @@ -81,12 +86,18 @@ public void handleResponse(String key, Message response) */ public void handleTimeout(String key) throws PipelineException { log.debug("Send timeout error to hub {}", key); - CommandData commandData = keyToRequest.remove(key); + RequestContext pending = keyToRequest.remove(key); ErrorData errorData = new ErrorData(ErrorType.OPERATION_TIMED_OUT, - String.format("Timeout for waiting response %s", commandData.toString()), + String.format("Timeout for waiting response on %s", pending.getPayload()), "Error in SpeakerWorkerService"); - ErrorMessage errorMessage = new ErrorMessage(errorData, System.currentTimeMillis(), key); + ErrorMessage errorMessage = new ErrorMessage(errorData, key, pending.getCookie()); carrier.sendResponse(key, errorMessage); } + + @Value + private static class RequestContext { + CommandData payload; + MessageCookie cookie; + } } diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/SwitchRuleServiceImpl.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/SwitchRuleServiceImpl.java deleted file mode 100644 index 665b361d1bb..00000000000 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/SwitchRuleServiceImpl.java +++ /dev/null @@ -1,231 +0,0 @@ -/* Copyright 2021 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.switchmanager.service.impl; - -import static java.lang.String.format; - -import org.openkilda.messaging.command.switches.SwitchRulesDeleteRequest; -import org.openkilda.messaging.command.switches.SwitchRulesInstallRequest; -import org.openkilda.messaging.error.ErrorData; -import org.openkilda.messaging.error.ErrorMessage; -import org.openkilda.messaging.error.ErrorType; -import org.openkilda.messaging.info.InfoMessage; -import org.openkilda.messaging.info.switches.SwitchRulesResponse; -import org.openkilda.model.FlowPath; -import org.openkilda.model.KildaFeatureToggles; -import org.openkilda.model.SwitchId; -import org.openkilda.model.SwitchProperties; -import org.openkilda.persistence.repositories.FlowPathRepository; -import org.openkilda.persistence.repositories.IslRepository; -import org.openkilda.persistence.repositories.KildaFeatureTogglesRepository; -import org.openkilda.persistence.repositories.RepositoryFactory; -import org.openkilda.persistence.repositories.SwitchPropertiesRepository; -import org.openkilda.persistence.repositories.SwitchRepository; -import org.openkilda.wfm.topology.switchmanager.service.SwitchManagerCarrier; -import org.openkilda.wfm.topology.switchmanager.service.SwitchRuleService; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -public class SwitchRuleServiceImpl implements SwitchRuleService { - - private SwitchManagerCarrier carrier; - private FlowPathRepository flowPathRepository; - private SwitchPropertiesRepository switchPropertiesRepository; - private KildaFeatureTogglesRepository featureTogglesRepository; - private IslRepository islRepository; - private SwitchRepository switchRepository; - - private boolean active = true; - - private boolean isOperationCompleted = true; - - public SwitchRuleServiceImpl(SwitchManagerCarrier carrier, RepositoryFactory repositoryFactory) { - flowPathRepository = repositoryFactory.createFlowPathRepository(); - switchPropertiesRepository = repositoryFactory.createSwitchPropertiesRepository(); - featureTogglesRepository = repositoryFactory.createFeatureTogglesRepository(); - islRepository = repositoryFactory.createIslRepository(); - switchRepository = repositoryFactory.createSwitchRepository(); - this.carrier = carrier; - } - - @Override - public void deleteRules(String key, SwitchRulesDeleteRequest data) { - isOperationCompleted = false; - SwitchId switchId = data.getSwitchId(); - if (!switchRepository.exists(switchId)) { - ErrorData errorData = new ErrorData(ErrorType.NOT_FOUND, format("Switch %s not found", switchId), - "Error when deleting switch rules"); - ErrorMessage errorMessage = new ErrorMessage(errorData, System.currentTimeMillis(), key); - - carrier.response(key, errorMessage); - return; - } - Optional switchProperties = switchPropertiesRepository.findBySwitchId(switchId); - KildaFeatureToggles featureToggles = featureTogglesRepository.getOrDefault(); - boolean server42FlowRttFeatureToggle = featureToggles.getServer42FlowRtt(); - data.setServer42FlowRttFeatureToggle(server42FlowRttFeatureToggle); - data.setServer42IslRttEnabled(featureToggles.getServer42IslRtt() - && switchProperties.map(SwitchProperties::hasServer42IslRttEnabled).orElse(false)); - - if (switchProperties.isPresent()) { - data.setMultiTable(switchProperties.get().isMultiTable()); - data.setSwitchLldp(switchProperties.get().isSwitchLldp()); - data.setSwitchArp(switchProperties.get().isSwitchArp()); - data.setServer42FlowRttSwitchProperty(switchProperties.get().isServer42FlowRtt()); - data.setServer42Port(switchProperties.get().getServer42Port()); - data.setServer42Vlan(switchProperties.get().getServer42Vlan()); - data.setServer42MacAddress(switchProperties.get().getServer42MacAddress()); - Collection flowPaths = flowPathRepository.findBySrcSwitch(switchId); - List flowPorts = new ArrayList<>(); - Set flowLldpPorts = new HashSet<>(); - Set flowArpPorts = new HashSet<>(); - Set server42FlowPorts = new HashSet<>(); - fillFlowPorts(switchProperties.get(), flowPaths, flowPorts, flowLldpPorts, flowArpPorts, server42FlowPorts, - server42FlowRttFeatureToggle && switchProperties.get().isServer42FlowRtt()); - - data.setFlowPorts(flowPorts); - data.setFlowLldpPorts(flowLldpPorts); - data.setFlowArpPorts(flowArpPorts); - data.setServer42FlowRttPorts(server42FlowPorts); - List islPorts = islRepository.findBySrcSwitch(switchId).stream() - .map(isl -> isl.getSrcPort()) - .collect(Collectors.toList()); - data.setIslPorts(islPorts); - } - carrier.sendCommandToSpeaker(key, data); - } - - @Override - public void installRules(String key, SwitchRulesInstallRequest data) { - isOperationCompleted = false; - SwitchId switchId = data.getSwitchId(); - if (!switchRepository.exists(switchId)) { - ErrorData errorData = new ErrorData(ErrorType.NOT_FOUND, format("Switch %s not found", switchId), - "Error when installing switch rules"); - ErrorMessage errorMessage = new ErrorMessage(errorData, System.currentTimeMillis(), key); - - carrier.response(key, errorMessage); - return; - } - Optional switchProperties = switchPropertiesRepository.findBySwitchId(switchId); - - KildaFeatureToggles featureToggles = featureTogglesRepository.getOrDefault(); - boolean server42FlowRttFeatureToggle = featureToggles.getServer42FlowRtt(); - data.setServer42FlowRttFeatureToggle(server42FlowRttFeatureToggle); - data.setServer42IslRttEnabled(featureToggles.getServer42IslRtt() - && switchProperties.map(SwitchProperties::hasServer42IslRttEnabled).orElse(false)); - - if (switchProperties.isPresent()) { - data.setMultiTable(switchProperties.get().isMultiTable()); - data.setSwitchLldp(switchProperties.get().isSwitchLldp()); - data.setSwitchArp(switchProperties.get().isSwitchArp()); - data.setServer42FlowRttSwitchProperty(switchProperties.get().isServer42FlowRtt()); - data.setServer42Port(switchProperties.get().getServer42Port()); - data.setServer42Vlan(switchProperties.get().getServer42Vlan()); - data.setServer42MacAddress(switchProperties.get().getServer42MacAddress()); - Collection flowPaths = flowPathRepository.findBySrcSwitch(switchId); - List flowPorts = new ArrayList<>(); - Set flowLldpPorts = new HashSet<>(); - Set flowArpPorts = new HashSet<>(); - Set server42FlowPorts = new HashSet<>(); - fillFlowPorts(switchProperties.get(), flowPaths, flowPorts, flowLldpPorts, flowArpPorts, server42FlowPorts, - server42FlowRttFeatureToggle && switchProperties.get().isServer42FlowRtt()); - data.setFlowPorts(flowPorts); - data.setFlowLldpPorts(flowLldpPorts); - data.setFlowArpPorts(flowArpPorts); - data.setServer42FlowRttPorts(server42FlowPorts); - List islPorts = islRepository.findBySrcSwitch(switchId).stream() - .map(isl -> isl.getSrcPort()) - .collect(Collectors.toList()); - data.setIslPorts(islPorts); - } - carrier.sendCommandToSpeaker(key, data); - } - - private void fillFlowPorts(SwitchProperties switchProperties, Collection flowPaths, - List flowPorts, Set flowLldpPorts, Set flowArpPorts, - Set server42FlowPorts, boolean server42Rtt) { - for (FlowPath flowPath : flowPaths) { - if (flowPath.isForward()) { - if (flowPath.isSrcWithMultiTable()) { - flowPorts.add(flowPath.getFlow().getSrcPort()); - if (server42Rtt && !flowPath.getFlow().isOneSwitchFlow()) { - server42FlowPorts.add(flowPath.getFlow().getSrcPort()); - } - } - if (flowPath.getFlow().getDetectConnectedDevices().isSrcLldp() - || switchProperties.isSwitchLldp()) { - flowLldpPorts.add(flowPath.getFlow().getSrcPort()); - } - if (flowPath.getFlow().getDetectConnectedDevices().isSrcArp() - || switchProperties.isSwitchArp()) { - flowArpPorts.add(flowPath.getFlow().getSrcPort()); - } - } else { - if (flowPath.isDestWithMultiTable()) { - flowPorts.add(flowPath.getFlow().getDestPort()); - if (server42Rtt && !flowPath.getFlow().isOneSwitchFlow()) { - server42FlowPorts.add(flowPath.getFlow().getDestPort()); - } - } - if (flowPath.getFlow().getDetectConnectedDevices().isDstLldp() - || switchProperties.isSwitchLldp()) { - flowLldpPorts.add(flowPath.getFlow().getDestPort()); - } - if (flowPath.getFlow().getDetectConnectedDevices().isDstArp() - || switchProperties.isSwitchArp()) { - flowArpPorts.add(flowPath.getFlow().getDestPort()); - } - } - } - } - - @Override - public void rulesResponse(String key, SwitchRulesResponse response) { - carrier.cancelTimeoutCallback(key); - InfoMessage message = new InfoMessage(response, System.currentTimeMillis(), key); - - carrier.response(key, message); - - isOperationCompleted = true; - - if (!active) { - carrier.sendInactive(); - } - } - - @Override - public void activate() { - active = true; - } - - @Override - public boolean deactivate() { - active = false; - return isAllOperationsCompleted(); - } - - @Override - public boolean isAllOperationsCompleted() { - return isOperationCompleted; - } -} diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/CreateLagPortServiceImpl.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/CreateLagPortServiceImpl.java deleted file mode 100644 index c895a4d4c0d..00000000000 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/CreateLagPortServiceImpl.java +++ /dev/null @@ -1,153 +0,0 @@ -/* Copyright 2021 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.switchmanager.service.impl.fsmhandlers; - -import static java.lang.String.format; - -import org.openkilda.messaging.error.ErrorMessage; -import org.openkilda.messaging.info.grpc.CreateLogicalPortResponse; -import org.openkilda.messaging.swmanager.request.CreateLagPortRequest; -import org.openkilda.wfm.share.utils.FsmExecutor; -import org.openkilda.wfm.topology.switchmanager.error.OperationTimeoutException; -import org.openkilda.wfm.topology.switchmanager.error.SpeakerFailureException; -import org.openkilda.wfm.topology.switchmanager.fsm.CreateLagPortFsm; -import org.openkilda.wfm.topology.switchmanager.fsm.CreateLagPortFsm.CreateLagContext; -import org.openkilda.wfm.topology.switchmanager.fsm.CreateLagPortFsm.CreateLagEvent; -import org.openkilda.wfm.topology.switchmanager.fsm.CreateLagPortFsm.CreateLagState; -import org.openkilda.wfm.topology.switchmanager.service.CreateLagPortService; -import org.openkilda.wfm.topology.switchmanager.service.LagPortOperationConfig; -import org.openkilda.wfm.topology.switchmanager.service.LagPortOperationService; -import org.openkilda.wfm.topology.switchmanager.service.SwitchManagerCarrier; - -import lombok.extern.slf4j.Slf4j; -import org.squirrelframework.foundation.fsm.StateMachineBuilder; - -import java.util.HashMap; -import java.util.Map; - -@Slf4j -public class CreateLagPortServiceImpl implements CreateLagPortService { - - private final SwitchManagerCarrier carrier; - private final LagPortOperationService lagPortOperationService; - private final Map fsms = new HashMap<>(); - private final StateMachineBuilder builder; - private final FsmExecutor fsmExecutor; - - private boolean active = true; - - public CreateLagPortServiceImpl(SwitchManagerCarrier carrier, LagPortOperationConfig config) { - this.lagPortOperationService = new LagPortOperationService(config); - this.builder = CreateLagPortFsm.builder(); - this.fsmExecutor = new FsmExecutor<>(CreateLagEvent.NEXT); - this.carrier = carrier; - } - - @Override - public void handleCreateLagRequest(String key, CreateLagPortRequest request) { - CreateLagPortFsm fsm = builder.newStateMachine(CreateLagState.START, carrier, key, request, - lagPortOperationService); - fsms.put(key, fsm); - - fsm.start(); - fireFsmEvent(fsm, CreateLagEvent.NEXT, CreateLagContext.builder().build()); - } - - @Override - public void handleTaskError(String key, ErrorMessage message) { - if (!fsms.containsKey(key)) { - logCreateFsmNotFound(key); - return; - } - CreateLagContext context = CreateLagContext.builder() - .error(new SpeakerFailureException(message.getData())) - .build(); - fireFsmEvent(fsms.get(key), CreateLagEvent.ERROR, context); - } - - - @Override - public void handleTaskTimeout(String key) { - if (!fsms.containsKey(key)) { - logCreateFsmNotFound(key); - return; - } - - CreateLagPortFsm fsm = fsms.get(key); - OperationTimeoutException error = new OperationTimeoutException( - format("LAG create operation timeout. Switch %s", fsm.getSwitchId())); - fireFsmEvent(fsm, CreateLagEvent.ERROR, CreateLagContext.builder().error(error).build()); - } - - @Override - public void handleGrpcResponse(String key, CreateLogicalPortResponse response) { - if (!fsms.containsKey(key)) { - // CreateLogicalPortResponse could belong to SwitchSyncFsm so log level is Info - logCreateFsmNotFound(key, true); - return; - } - fireFsmEvent(fsms.get(key), CreateLagEvent.LAG_INSTALLED, - CreateLagContext.builder().createdLogicalPort(response.getLogicalPort()).build()); - } - - @Override - public void activate() { - active = true; - } - - @Override - public boolean deactivate() { - active = false; - return isAllOperationsCompleted(); - } - - @Override - public boolean isAllOperationsCompleted() { - return fsms.isEmpty(); - } - - private void logCreateFsmNotFound(String key) { - logCreateFsmNotFound(key, false); - } - - private void logCreateFsmNotFound(String key, boolean info) { - String message = "Create LAG fsm with key {} not found"; - if (info) { - log.info(message, key); - } else { - log.warn(message, key); - } - } - - private void fireFsmEvent(CreateLagPortFsm fsm, CreateLagEvent event, CreateLagContext context) { - fsmExecutor.fire(fsm, event, context); - removeIfCompleted(fsm); - } - - private void removeIfCompleted(CreateLagPortFsm fsm) { - if (fsm.isTerminated()) { - String requestKey = fsm.getKey(); - log.info("Create LAG {} FSM have reached termination state (key={})", fsm.getRequest(), requestKey); - - fsms.remove(requestKey); - carrier.cancelTimeoutCallback(requestKey); - - if (isAllOperationsCompleted() && !active) { - carrier.sendInactive(); - } - } - } -} diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/DeleteLagPortServiceImpl.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/DeleteLagPortServiceImpl.java deleted file mode 100644 index 06209682340..00000000000 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/DeleteLagPortServiceImpl.java +++ /dev/null @@ -1,152 +0,0 @@ -/* Copyright 2021 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.switchmanager.service.impl.fsmhandlers; - -import static java.lang.String.format; - -import org.openkilda.messaging.error.ErrorMessage; -import org.openkilda.messaging.info.grpc.DeleteLogicalPortResponse; -import org.openkilda.messaging.swmanager.request.DeleteLagPortRequest; -import org.openkilda.wfm.share.utils.FsmExecutor; -import org.openkilda.wfm.topology.switchmanager.error.OperationTimeoutException; -import org.openkilda.wfm.topology.switchmanager.error.SpeakerFailureException; -import org.openkilda.wfm.topology.switchmanager.fsm.DeleteLagPortFsm; -import org.openkilda.wfm.topology.switchmanager.fsm.DeleteLagPortFsm.DeleteLagContext; -import org.openkilda.wfm.topology.switchmanager.fsm.DeleteLagPortFsm.DeleteLagEvent; -import org.openkilda.wfm.topology.switchmanager.fsm.DeleteLagPortFsm.DeleteLagState; -import org.openkilda.wfm.topology.switchmanager.service.DeleteLagPortService; -import org.openkilda.wfm.topology.switchmanager.service.LagPortOperationConfig; -import org.openkilda.wfm.topology.switchmanager.service.LagPortOperationService; -import org.openkilda.wfm.topology.switchmanager.service.SwitchManagerCarrier; - -import lombok.extern.slf4j.Slf4j; -import org.squirrelframework.foundation.fsm.StateMachineBuilder; - -import java.util.HashMap; -import java.util.Map; - -@Slf4j -public class DeleteLagPortServiceImpl implements DeleteLagPortService { - - private final SwitchManagerCarrier carrier; - private final LagPortOperationService lagOperationService; - private final Map fsms = new HashMap<>(); - private final StateMachineBuilder builder; - private final FsmExecutor fsmExecutor; - - private boolean active = true; - - public DeleteLagPortServiceImpl(SwitchManagerCarrier carrier, LagPortOperationConfig config) { - this.lagOperationService = new LagPortOperationService(config); - this.builder = DeleteLagPortFsm.builder(); - this.fsmExecutor = new FsmExecutor<>(DeleteLagEvent.NEXT); - this.carrier = carrier; - } - - @Override - public void handleDeleteLagRequest(String key, DeleteLagPortRequest request) { - DeleteLagPortFsm fsm = builder.newStateMachine( - DeleteLagState.START, carrier, key, request, lagOperationService); - fsms.put(key, fsm); - - fsm.start(); - fireFsmEvent(fsm, DeleteLagEvent.NEXT, DeleteLagContext.builder().build()); - } - - @Override - public void handleTaskError(String key, ErrorMessage message) { - if (!fsms.containsKey(key)) { - logDeleteFsmNotFound(key); - return; - } - DeleteLagContext context = DeleteLagContext.builder() - .error(new SpeakerFailureException(message.getData())) - .build(); - fireFsmEvent(fsms.get(key), DeleteLagEvent.ERROR, context); - } - - - @Override - public void handleTaskTimeout(String key) { - if (!fsms.containsKey(key)) { - logDeleteFsmNotFound(key); - return; - } - - DeleteLagPortFsm fsm = fsms.get(key); - OperationTimeoutException error = new OperationTimeoutException( - format("LAG delete operation timeout. Switch %s", fsm.getSwitchId())); - fireFsmEvent(fsm, DeleteLagEvent.ERROR, DeleteLagContext.builder().error(error).build()); - } - - @Override - public void handleGrpcResponse(String key, DeleteLogicalPortResponse response) { - if (!fsms.containsKey(key)) { - // DeleteLogicalPortResponse could belong to SwitchSyncFsm so log level is Info - logDeleteFsmNotFound(key, true); - return; - } - fireFsmEvent(fsms.get(key), DeleteLagEvent.LAG_REMOVED, - DeleteLagContext.builder().deletedLogicalPort(response.getLogicalPortNumber()).build()); - } - - @Override - public void activate() { - active = true; - } - - @Override - public boolean deactivate() { - active = false; - return isAllOperationsCompleted(); - } - - @Override - public boolean isAllOperationsCompleted() { - return fsms.isEmpty(); - } - - private void logDeleteFsmNotFound(String key) { - logDeleteFsmNotFound(key, false); - } - - private void logDeleteFsmNotFound(String key, boolean info) { - String message = "Delete LAG fsm with key {} not found"; - if (info) { - log.info(message, key); - } else { - log.warn(message, key); - } - } - - private void fireFsmEvent(DeleteLagPortFsm fsm, DeleteLagEvent event, DeleteLagContext context) { - fsmExecutor.fire(fsm, event, context); - removeIfCompleted(fsm); - } - - private void removeIfCompleted(DeleteLagPortFsm fsm) { - if (fsm.isTerminated()) { - String requestKey = fsm.getKey(); - log.info("Delete LAG {} FSM have reached termination state (key={})", fsm.getRequest(), requestKey); - fsms.remove(requestKey); - carrier.cancelTimeoutCallback(requestKey); - - if (isAllOperationsCompleted() && !active) { - carrier.sendInactive(); - } - } - } -} diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/SwitchSyncServiceImpl.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/SwitchSyncServiceImpl.java deleted file mode 100644 index d09c44e7cc3..00000000000 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/SwitchSyncServiceImpl.java +++ /dev/null @@ -1,271 +0,0 @@ -/* 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.wfm.topology.switchmanager.service.impl.fsmhandlers; - -import org.openkilda.messaging.command.switches.SwitchValidateRequest; -import org.openkilda.messaging.error.ErrorMessage; -import org.openkilda.messaging.info.flow.FlowReinstallResponse; -import org.openkilda.persistence.PersistenceManager; -import org.openkilda.wfm.share.flow.resources.FlowResourcesConfig; -import org.openkilda.wfm.topology.switchmanager.fsm.SwitchSyncFsm; -import org.openkilda.wfm.topology.switchmanager.fsm.SwitchSyncFsm.SwitchSyncEvent; -import org.openkilda.wfm.topology.switchmanager.fsm.SwitchSyncFsm.SwitchSyncState; -import org.openkilda.wfm.topology.switchmanager.model.ValidationResult; -import org.openkilda.wfm.topology.switchmanager.service.CommandBuilder; -import org.openkilda.wfm.topology.switchmanager.service.SwitchManagerCarrier; -import org.openkilda.wfm.topology.switchmanager.service.SwitchSyncService; -import org.openkilda.wfm.topology.switchmanager.service.impl.CommandBuilderImpl; - -import com.google.common.annotations.VisibleForTesting; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.squirrelframework.foundation.fsm.StateMachineBuilder; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Slf4j -public class SwitchSyncServiceImpl implements SwitchSyncService { - - private Map fsms = new HashMap<>(); - - @Getter - private boolean active = true; - - @VisibleForTesting - CommandBuilder commandBuilder; - private SwitchManagerCarrier carrier; - private StateMachineBuilder builder; - - public SwitchSyncServiceImpl(SwitchManagerCarrier carrier, PersistenceManager persistenceManager, - FlowResourcesConfig flowResourcesConfig) { - this.carrier = carrier; - this.commandBuilder = new CommandBuilderImpl(persistenceManager, flowResourcesConfig); - this.builder = SwitchSyncFsm.builder(); - } - - @Override - public void handleSwitchSync(String key, SwitchValidateRequest request, ValidationResult validationResult) { - SwitchSyncFsm fsm = - builder.newStateMachine(SwitchSyncState.INITIALIZED, carrier, key, commandBuilder, request, - validationResult); - - process(fsm); - } - - @Override - public void handleInstallRulesResponse(String key) { - SwitchSyncFsm fsm = fsms.get(key); - if (fsm == null) { - logFsmNotFound(key); - return; - } - - fsm.fire(SwitchSyncEvent.MISSING_RULES_INSTALLED); - process(fsm); - } - - @Override - public void handleRemoveRulesResponse(String key) { - SwitchSyncFsm fsm = fsms.get(key); - if (fsm == null) { - logFsmNotFound(key); - return; - } - - fsm.fire(SwitchSyncEvent.EXCESS_RULES_REMOVED); - process(fsm); - } - - @Override - public void handleReinstallDefaultRulesResponse(String key, FlowReinstallResponse response) { - SwitchSyncFsm fsm = fsms.get(key); - if (fsm == null) { - logFsmNotFound(key); - return; - } - - fsm.fire(SwitchSyncEvent.MISCONFIGURED_RULES_REINSTALLED, response); - process(fsm); - } - - @Override - public void handleRemoveMetersResponse(String key) { - SwitchSyncFsm fsm = fsms.get(key); - if (fsm == null) { - logFsmNotFound(key); - return; - } - - fsm.fire(SwitchSyncEvent.METERS_REMOVED); - process(fsm); - } - - @Override - public void handleModifyMetersResponse(String key) { - SwitchSyncFsm fsm = fsms.get(key); - if (fsm == null) { - logFsmNotFound(key); - return; - } - - fsm.fire(SwitchSyncEvent.MISCONFIGURED_METERS_MODIFIED); - process(fsm); - } - - @Override - public void handleInstallGroupResponse(String key) { - SwitchSyncFsm fsm = fsms.get(key); - if (fsm == null) { - logFsmNotFound(key); - return; - } - - fsm.fire(SwitchSyncEvent.GROUPS_INSTALLED); - process(fsm); - } - - @Override - public void handleModifyGroupResponse(String key) { - SwitchSyncFsm fsm = fsms.get(key); - if (fsm == null) { - logFsmNotFound(key); - return; - } - - fsm.fire(SwitchSyncEvent.GROUPS_MODIFIED); - process(fsm); - } - - @Override - public void handleDeleteGroupResponse(String key) { - SwitchSyncFsm fsm = fsms.get(key); - if (fsm == null) { - logFsmNotFound(key); - return; - } - - fsm.fire(SwitchSyncEvent.GROUPS_REMOVED); - process(fsm); - } - - @Override - public void handleCreateLogicalPortResponse(String key) { - SwitchSyncFsm fsm = fsms.get(key); - if (fsm == null) { - // InstallLogicalPortResponse could belong to CreateLagFsm so log level is Info - logFsmNotFound(key, true); - return; - } - - fsm.fire(SwitchSyncEvent.LOGICAL_PORT_INSTALLED); - process(fsm); - } - - @Override - public void handleDeleteLogicalPortResponse(String key) { - SwitchSyncFsm fsm = fsms.get(key); - if (fsm == null) { - // DeleteLogicalPortResponse could belong to DeleteLagFsm so log level is Info - logFsmNotFound(key, true); - return; - } - - fsm.fire(SwitchSyncEvent.LOGICAL_PORT_REMOVED); - process(fsm); - } - - @Override - public void handleTaskTimeout(String key) { - SwitchSyncFsm fsm = fsms.get(key); - if (fsm == null) { - return; - } - - fsm.fire(SwitchSyncEvent.TIMEOUT); - process(fsm); - } - - @Override - public void handleTaskError(String key, ErrorMessage message) { - SwitchSyncFsm fsm = fsms.get(key); - if (fsm == null) { - return; - } - - fsm.fire(SwitchSyncEvent.ERROR, message); - process(fsm); - } - - private void logFsmNotFound(String key) { - logFsmNotFound(key, false); - } - - private void logFsmNotFound(String key, boolean info) { - String message = "Switch sync FSM with key {} not found"; - if (info) { - log.info(message, key); - } else { - log.warn(message, key); - } - } - - void process(SwitchSyncFsm fsm) { - final List stopStates = Arrays.asList( - SwitchSyncState.RULES_COMMANDS_SEND, - SwitchSyncState.METERS_COMMANDS_SEND, - SwitchSyncState.GROUPS_COMMANDS_SEND, - SwitchSyncState.LOGICAL_PORTS_COMMANDS_SEND, - SwitchSyncState.FINISHED, - SwitchSyncState.FINISHED_WITH_ERROR - ); - - while (!stopStates.contains(fsm.getCurrentState())) { - fsms.put(fsm.getKey(), fsm); - fsm.fire(SwitchSyncEvent.NEXT); - } - - final List exitStates = Arrays.asList( - SwitchSyncState.FINISHED, - SwitchSyncState.FINISHED_WITH_ERROR - ); - - if (exitStates.contains(fsm.getCurrentState())) { - fsms.remove(fsm.getKey()); - if (isAllOperationsCompleted() && !active) { - carrier.sendInactive(); - } - } - } - - @Override - public void activate() { - active = true; - } - - @Override - public boolean deactivate() { - active = false; - return isAllOperationsCompleted(); - } - - @Override - public boolean isAllOperationsCompleted() { - return fsms.isEmpty(); - } -} diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/SwitchValidateServiceImpl.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/SwitchValidateServiceImpl.java deleted file mode 100644 index 122b4b78752..00000000000 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/SwitchValidateServiceImpl.java +++ /dev/null @@ -1,195 +0,0 @@ -/* 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.wfm.topology.switchmanager.service.impl.fsmhandlers; - -import org.openkilda.messaging.command.switches.SwitchValidateRequest; -import org.openkilda.messaging.error.ErrorMessage; -import org.openkilda.messaging.info.flow.FlowDumpResponse; -import org.openkilda.messaging.info.group.GroupDumpResponse; -import org.openkilda.messaging.info.grpc.DumpLogicalPortsResponse; -import org.openkilda.messaging.info.meter.MeterDumpResponse; -import org.openkilda.persistence.PersistenceManager; -import org.openkilda.rulemanager.RuleManager; -import org.openkilda.wfm.share.metrics.MeterRegistryHolder; -import org.openkilda.wfm.share.utils.FsmExecutor; -import org.openkilda.wfm.topology.switchmanager.error.OperationTimeoutException; -import org.openkilda.wfm.topology.switchmanager.error.SpeakerFailureException; -import org.openkilda.wfm.topology.switchmanager.fsm.SwitchValidateFsm; -import org.openkilda.wfm.topology.switchmanager.fsm.SwitchValidateFsm.SwitchValidateContext; -import org.openkilda.wfm.topology.switchmanager.fsm.SwitchValidateFsm.SwitchValidateEvent; -import org.openkilda.wfm.topology.switchmanager.fsm.SwitchValidateFsm.SwitchValidateState; -import org.openkilda.wfm.topology.switchmanager.service.SwitchManagerCarrier; -import org.openkilda.wfm.topology.switchmanager.service.SwitchValidateService; -import org.openkilda.wfm.topology.switchmanager.service.ValidationService; - -import io.micrometer.core.instrument.LongTaskTimer; -import io.micrometer.core.instrument.LongTaskTimer.Sample; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.squirrelframework.foundation.fsm.StateMachineBuilder; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.TimeUnit; - -@Slf4j -public class SwitchValidateServiceImpl implements SwitchValidateService { - - private final Map fsms = new HashMap<>(); - - private final ValidationService validationService; - private final RuleManager ruleManager; - private final SwitchManagerCarrier carrier; - private final StateMachineBuilder< - SwitchValidateFsm, SwitchValidateState, SwitchValidateEvent, SwitchValidateContext> builder; - private final FsmExecutor< - SwitchValidateFsm, SwitchValidateState, SwitchValidateEvent, SwitchValidateContext> fsmExecutor; - - private final PersistenceManager persistenceManager; - - @Getter - private boolean active = true; - - public SwitchValidateServiceImpl(SwitchManagerCarrier carrier, PersistenceManager persistenceManager, - ValidationService validationService, RuleManager ruleManager) { - this.carrier = carrier; - this.builder = SwitchValidateFsm.builder(); - this.fsmExecutor = new FsmExecutor<>(SwitchValidateEvent.NEXT); - this.validationService = validationService; - this.ruleManager = ruleManager; - this.persistenceManager = persistenceManager; - } - - @Override - public void handleSwitchValidateRequest(String key, SwitchValidateRequest request) { - SwitchValidateFsm fsm = - builder.newStateMachine( - SwitchValidateState.START, carrier, key, request, validationService, ruleManager, - persistenceManager); - MeterRegistryHolder.getRegistry().ifPresent(registry -> { - Sample sample = LongTaskTimer.builder("fsm.active_execution") - .register(registry) - .start(); - fsm.addTerminateListener(e -> { - long duration = sample.stop(); - if (fsm.getCurrentState() == SwitchValidateState.FINISHED) { - registry.timer("fsm.execution.success") - .record(duration, TimeUnit.NANOSECONDS); - } else if (fsm.getCurrentState() == SwitchValidateState.FINISHED_WITH_ERROR) { - registry.timer("fsm.execution.failed") - .record(duration, TimeUnit.NANOSECONDS); - } - }); - }); - fsms.put(key, fsm); - - fsm.start(); - handle(fsm, SwitchValidateEvent.NEXT, SwitchValidateContext.builder().build()); - } - - @Override - public void handleFlowEntriesResponse(String key, FlowDumpResponse data) { - handle(key, SwitchValidateEvent.RULES_RECEIVED, - SwitchValidateContext.builder().flowEntries(data.getFlowSpeakerData()).build()); - } - - @Override - public void handleGroupEntriesResponse(String key, GroupDumpResponse data) { - handle(key, SwitchValidateEvent.GROUPS_RECEIVED, - SwitchValidateContext.builder().groupEntries(data.getGroupSpeakerData()).build()); - } - - @Override - public void handleLogicalPortResponse(String key, DumpLogicalPortsResponse data) { - handle(key, SwitchValidateEvent.LOGICAL_PORTS_RECEIVED, SwitchValidateContext.builder() - .logicalPortEntries(data.getLogicalPorts()).build()); - } - - @Override - public void handleMeterEntriesResponse(String key, MeterDumpResponse data) { - handle(key, SwitchValidateEvent.METERS_RECEIVED, - SwitchValidateContext.builder().meterEntries(data.getMeterSpeakerData()).build()); - } - - @Override - public void handleMetersUnsupportedResponse(String key) { - handle(key, SwitchValidateEvent.METERS_UNSUPPORTED, SwitchValidateContext.builder().build()); - } - - @Override - public void handleTaskError(String key, ErrorMessage message) { - SwitchValidateContext context = SwitchValidateContext.builder() - .error(new SpeakerFailureException(message.getData())) - .build(); - handle(key, SwitchValidateEvent.ERROR, context); - } - - @Override - public void handleTaskTimeout(String key) { - Optional potential = lookupFsm(key); - if (potential.isPresent()) { - SwitchValidateFsm fsm = potential.get(); - OperationTimeoutException error = new OperationTimeoutException(fsm.getSwitchId()); - handle(fsm, SwitchValidateEvent.ERROR, SwitchValidateContext.builder().error(error).build()); - } - } - - private void handle(String key, SwitchValidateEvent event, SwitchValidateContext context) { - Optional fsm = lookupFsm(key); - if (!fsm.isPresent()) { - log.warn("Switch validate FSM with key {} not found", key); - return; - } - handle(fsm.get(), event, context); - } - - private void handle(SwitchValidateFsm fsm, SwitchValidateEvent event, SwitchValidateContext context) { - fsmExecutor.fire(fsm, event, context); - removeIfCompleted(fsm); - } - - private Optional lookupFsm(String key) { - return Optional.ofNullable(fsms.get(key)); - } - - void removeIfCompleted(SwitchValidateFsm fsm) { - if (fsm.isTerminated()) { - log.info("Switch {} validation FSM have reached termination state (key={})", - fsm.getSwitchId(), fsm.getKey()); - fsms.remove(fsm.getKey()); - if (isAllOperationsCompleted() && !active) { - carrier.sendInactive(); - } - } - } - - @Override - public void activate() { - active = true; - } - - @Override - public boolean deactivate() { - active = false; - return isAllOperationsCompleted(); - } - - @Override - public boolean isAllOperationsCompleted() { - return fsms.isEmpty(); - } -} diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/SwitchSyncServiceImplTest.java b/src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/SwitchSyncServiceTest.java similarity index 79% rename from src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/SwitchSyncServiceImplTest.java rename to src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/SwitchSyncServiceTest.java index da9541f72df..5130ecfe58f 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/SwitchSyncServiceImplTest.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/SwitchSyncServiceTest.java @@ -1,4 +1,4 @@ -/* Copyright 2019 Telstra Open Source +/* Copyright 2022 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,7 +13,7 @@ * limitations under the License. */ -package org.openkilda.wfm.topology.switchmanager.service.impl.fsmhandlers; +package org.openkilda.wfm.topology.switchmanager.service; import static com.google.common.collect.Sets.newHashSet; import static java.util.Collections.emptyList; @@ -25,10 +25,9 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; -import org.openkilda.config.provider.PropertiesBasedConfigurationProvider; +import org.openkilda.messaging.MessageCookie; import org.openkilda.messaging.command.CommandData; import org.openkilda.messaging.command.flow.InstallFlowForSwitchManagerRequest; import org.openkilda.messaging.command.flow.InstallIngressFlow; @@ -40,7 +39,10 @@ import org.openkilda.messaging.error.ErrorMessage; import org.openkilda.messaging.error.ErrorType; import org.openkilda.messaging.info.InfoMessage; +import org.openkilda.messaging.info.flow.FlowInstallResponse; +import org.openkilda.messaging.info.flow.FlowRemoveResponse; import org.openkilda.messaging.info.rule.FlowEntry; +import org.openkilda.messaging.info.switches.DeleteMeterResponse; import org.openkilda.messaging.info.switches.MeterInfoEntry; import org.openkilda.messaging.info.switches.SwitchSyncResponse; import org.openkilda.model.FlowEncapsulationType; @@ -48,34 +50,26 @@ import org.openkilda.model.OutputVlanType; import org.openkilda.model.SwitchId; import org.openkilda.model.cookie.FlowSegmentCookie; -import org.openkilda.persistence.PersistenceManager; -import org.openkilda.persistence.repositories.FlowPathRepository; -import org.openkilda.persistence.repositories.FlowRepository; -import org.openkilda.persistence.repositories.RepositoryFactory; -import org.openkilda.persistence.repositories.TransitVlanRepository; -import org.openkilda.wfm.share.flow.resources.FlowResourcesConfig; +import org.openkilda.wfm.error.MessageDispatchException; +import org.openkilda.wfm.error.UnexpectedInputException; import org.openkilda.wfm.topology.switchmanager.model.ValidateGroupsResult; import org.openkilda.wfm.topology.switchmanager.model.ValidateLogicalPortsResult; import org.openkilda.wfm.topology.switchmanager.model.ValidateMetersResult; import org.openkilda.wfm.topology.switchmanager.model.ValidateRulesResult; import org.openkilda.wfm.topology.switchmanager.model.ValidationResult; -import org.openkilda.wfm.topology.switchmanager.service.CommandBuilder; -import org.openkilda.wfm.topology.switchmanager.service.SwitchManagerCarrier; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import java.util.List; -import java.util.Properties; import java.util.UUID; @RunWith(MockitoJUnitRunner.class) -public class SwitchSyncServiceImplTest { +public class SwitchSyncServiceTest { private static SwitchId SWITCH_ID = new SwitchId(0x0000000000000001L); private static SwitchId INGRESS_SWITCH_ID = new SwitchId(0x0000000000000002L); @@ -87,13 +81,10 @@ public class SwitchSyncServiceImplTest { @Mock private SwitchManagerCarrier carrier; - @Mock - private PersistenceManager persistenceManager; - @Mock private CommandBuilder commandBuilder; - private SwitchSyncServiceImpl service; + private SwitchSyncService service; private SwitchValidateRequest request; private FlowEntry flowEntry; @@ -105,26 +96,7 @@ public class SwitchSyncServiceImplTest { @Before public void setUp() { - RepositoryFactory repositoryFactory = Mockito.mock(RepositoryFactory.class); - FlowRepository flowRepository = Mockito.mock(FlowRepository.class); - FlowPathRepository flowPathRepository = Mockito.mock(FlowPathRepository.class); - TransitVlanRepository transitVlanRepository = Mockito.mock(TransitVlanRepository.class); - - when(repositoryFactory.createFlowPathRepository()).thenReturn(flowPathRepository); - when(repositoryFactory.createFlowRepository()).thenReturn(flowRepository); - when(repositoryFactory.createTransitVlanRepository()).thenReturn(transitVlanRepository); - when(persistenceManager.getRepositoryFactory()).thenReturn(repositoryFactory); - - Properties configProps = new Properties(); - configProps.setProperty("flow.meter-id.max", "40"); - configProps.setProperty("flow.vlan.max", "50"); - - PropertiesBasedConfigurationProvider configurationProvider = - new PropertiesBasedConfigurationProvider(configProps); - FlowResourcesConfig flowResourcesConfig = configurationProvider.getConfiguration(FlowResourcesConfig.class); - - service = new SwitchSyncServiceImpl(carrier, persistenceManager, flowResourcesConfig); - service.commandBuilder = commandBuilder; + service = new SwitchSyncService(carrier, commandBuilder); request = SwitchValidateRequest.builder().switchId(SWITCH_ID).performSync(true).build(); flowEntry = new FlowEntry( @@ -175,22 +147,19 @@ public void handleCommandBuilderMissingRulesException() { verifyNoMoreInteractions(carrier); } - @Test - public void doNothingWhenFsmNotFound() { - service.handleInstallRulesResponse(KEY); - - verifyZeroInteractions(carrier); - verifyZeroInteractions(commandBuilder); + @Test(expected = MessageDispatchException.class) + public void reportErrorInCaseOfMissingHandler() throws UnexpectedInputException, MessageDispatchException { + service.dispatchWorkerMessage(new FlowInstallResponse(), new MessageCookie("dummy")); } @Test - public void handleRuleSyncSuccess() { + public void handleRuleSyncSuccess() throws UnexpectedInputException, MessageDispatchException { service.handleSwitchSync(KEY, request, makeValidationResult()); verify(commandBuilder).buildCommandsToSyncMissingRules(eq(SWITCH_ID), eq(missingRules)); verify(carrier).sendCommandToSpeaker(eq(KEY), any(CommandData.class)); - service.handleInstallRulesResponse(KEY); + service.dispatchWorkerMessage(new FlowInstallResponse(), new MessageCookie(KEY)); verify(carrier).cancelTimeoutCallback(eq(KEY)); verify(carrier).response(eq(KEY), any(InfoMessage.class)); @@ -200,13 +169,13 @@ public void handleRuleSyncSuccess() { } @Test - public void receiveRuleSyncTimeout() { + public void receiveRuleSyncTimeout() throws MessageDispatchException { service.handleSwitchSync(KEY, request, makeValidationResult()); verify(commandBuilder).buildCommandsToSyncMissingRules(eq(SWITCH_ID), eq(missingRules)); verify(carrier).sendCommandToSpeaker(eq(KEY), any(CommandData.class)); - service.handleTaskTimeout(KEY); + service.timeout(new MessageCookie(KEY)); verify(carrier).response(eq(KEY), any(ErrorMessage.class)); @@ -215,14 +184,14 @@ public void receiveRuleSyncTimeout() { } @Test - public void receiveRuleSyncError() { + public void receiveRuleSyncError() throws MessageDispatchException { service.handleSwitchSync(KEY, request, makeValidationResult()); verify(commandBuilder).buildCommandsToSyncMissingRules(eq(SWITCH_ID), eq(missingRules)); verify(carrier).sendCommandToSpeaker(eq(KEY), any(InstallFlowForSwitchManagerRequest.class)); ErrorMessage errorMessage = getErrorMessage(); - service.handleTaskError(KEY, errorMessage); + service.dispatchWorkerMessage(errorMessage.getData(), new MessageCookie(KEY)); verify(carrier).cancelTimeoutCallback(eq(KEY)); verify(carrier).response(eq(KEY), any(ErrorMessage.class)); @@ -232,7 +201,7 @@ public void receiveRuleSyncError() { } @Test - public void receiveMetersSyncError() { + public void receiveMetersSyncError() throws MessageDispatchException { request = SwitchValidateRequest.builder().switchId(SWITCH_ID).performSync(true).removeExcess(true).build(); missingRules = emptyList(); excessMeters = singletonList( @@ -241,8 +210,7 @@ public void receiveMetersSyncError() { service.handleSwitchSync(KEY, request, makeValidationResult()); verify(carrier).sendCommandToSpeaker(eq(KEY), any(CommandData.class)); - ErrorMessage errorMessage = getErrorMessage(); - service.handleTaskError(KEY, errorMessage); + service.dispatchWorkerMessage(getErrorMessage().getData(), new MessageCookie(KEY)); verify(carrier).cancelTimeoutCallback(eq(KEY)); verify(carrier).response(eq(KEY), any(ErrorMessage.class)); @@ -266,7 +234,7 @@ public void handleNothingToSyncWithExcess() { } @Test - public void handleSyncExcess() { + public void handleSyncExcess() throws UnexpectedInputException, MessageDispatchException { request = SwitchValidateRequest.builder().switchId(SWITCH_ID).performSync(true).removeExcess(true).build(); excessRules = singletonList(EXCESS_COOKIE); @@ -290,12 +258,12 @@ public void handleSyncExcess() { eq(SWITCH_ID), eq(singletonList(flowEntry)), eq(excessRules)); verify(carrier).sendCommandToSpeaker(eq(KEY), any(DeleterMeterForSwitchManagerRequest.class)); - service.handleRemoveMetersResponse(KEY); + service.dispatchWorkerMessage(new DeleteMeterResponse(true), new MessageCookie(KEY)); verify(carrier).sendCommandToSpeaker(eq(KEY), any(InstallFlowForSwitchManagerRequest.class)); verify(carrier).sendCommandToSpeaker(eq(KEY), any(RemoveFlowForSwitchManagerRequest.class)); - service.handleInstallRulesResponse(KEY); - service.handleRemoveRulesResponse(KEY); + service.dispatchWorkerMessage(new FlowInstallResponse(), new MessageCookie(KEY)); + service.dispatchWorkerMessage(new FlowRemoveResponse(), new MessageCookie(KEY)); verify(carrier, times(3)).sendCommandToSpeaker(eq(KEY), any(CommandData.class)); @@ -307,7 +275,7 @@ public void handleSyncExcess() { } @Test - public void handleSyncOnlyExcessMeters() { + public void handleSyncOnlyExcessMeters() throws UnexpectedInputException, MessageDispatchException { request = SwitchValidateRequest.builder().switchId(SWITCH_ID).performSync(true).removeExcess(true).build(); missingRules = emptyList(); excessMeters = singletonList( @@ -316,7 +284,7 @@ public void handleSyncOnlyExcessMeters() { service.handleSwitchSync(KEY, request, makeValidationResult()); verify(carrier).sendCommandToSpeaker(eq(KEY), any(CommandData.class)); - service.handleRemoveMetersResponse(KEY); + service.dispatchWorkerMessage(new DeleteMeterResponse(true), new MessageCookie(KEY)); verify(carrier).cancelTimeoutCallback(eq(KEY)); verify(carrier).response(eq(KEY), any(InfoMessage.class)); @@ -326,7 +294,7 @@ public void handleSyncOnlyExcessMeters() { } @Test - public void handleSyncWhenNotProcessMeters() { + public void handleSyncWhenNotProcessMeters() throws UnexpectedInputException, MessageDispatchException { request = SwitchValidateRequest.builder().switchId(SWITCH_ID).performSync(true).removeExcess(true).build(); ValidationResult tempResult = makeValidationResult(); @@ -338,7 +306,7 @@ public void handleSyncWhenNotProcessMeters() { verify(commandBuilder).buildCommandsToSyncMissingRules(eq(SWITCH_ID), eq(missingRules)); verify(carrier).sendCommandToSpeaker(eq(KEY), any(CommandData.class)); - service.handleInstallRulesResponse(KEY); + service.dispatchWorkerMessage(new FlowInstallResponse(), new MessageCookie(KEY)); verify(carrier).cancelTimeoutCallback(eq(KEY)); ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(InfoMessage.class); diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/SwitchValidateServiceImplTest.java b/src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/SwitchValidateServiceTest.java similarity index 78% rename from src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/SwitchValidateServiceImplTest.java rename to src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/SwitchValidateServiceTest.java index d5e1a31d55b..73339c18521 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/impl/fsmhandlers/SwitchValidateServiceImplTest.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/SwitchValidateServiceTest.java @@ -1,4 +1,4 @@ -/* Copyright 2021 Telstra Open Source +/* Copyright 2022 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,7 +13,7 @@ * limitations under the License. */ -package org.openkilda.wfm.topology.switchmanager.service.impl.fsmhandlers; +package org.openkilda.wfm.topology.switchmanager.service; import static com.google.common.collect.Sets.newHashSet; import static java.util.Collections.emptyList; @@ -26,10 +26,10 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.openkilda.model.SwitchFeature.LAG; +import org.openkilda.messaging.MessageCookie; import org.openkilda.messaging.command.CommandData; import org.openkilda.messaging.command.switches.SwitchValidateRequest; import org.openkilda.messaging.error.ErrorData; @@ -39,6 +39,7 @@ import org.openkilda.messaging.info.flow.FlowDumpResponse; import org.openkilda.messaging.info.group.GroupDumpResponse; import org.openkilda.messaging.info.meter.MeterDumpResponse; +import org.openkilda.messaging.info.meter.SwitchMeterUnsupported; import org.openkilda.messaging.info.switches.SwitchValidationResponse; import org.openkilda.model.MeterId; import org.openkilda.model.Switch; @@ -56,10 +57,11 @@ import org.openkilda.rulemanager.OfTable; import org.openkilda.rulemanager.OfVersion; import org.openkilda.rulemanager.RuleManager; +import org.openkilda.wfm.error.MessageDispatchException; +import org.openkilda.wfm.error.UnexpectedInputException; import org.openkilda.wfm.topology.switchmanager.model.ValidateMetersResult; import org.openkilda.wfm.topology.switchmanager.model.ValidateRulesResult; import org.openkilda.wfm.topology.switchmanager.model.ValidationResult; -import org.openkilda.wfm.topology.switchmanager.service.SwitchManagerCarrier; import org.openkilda.wfm.topology.switchmanager.service.impl.ValidationServiceImpl; import com.google.common.collect.Sets; @@ -74,8 +76,7 @@ import java.util.Optional; @RunWith(MockitoJUnitRunner.class) -public class SwitchValidateServiceImplTest { - +public class SwitchValidateServiceTest { private static final SwitchId SWITCH_ID = new SwitchId(0x0000000000000001L); private static final SwitchId SWITCH_ID_MISSING = new SwitchId(0x0000000000000002L); private static final Switch SWITCH_1 = Switch.builder().switchId(SWITCH_ID).features(Sets.newHashSet(LAG)).build(); @@ -93,7 +94,7 @@ public class SwitchValidateServiceImplTest { @Mock private SwitchManagerCarrier carrier; - private SwitchValidateServiceImpl service; + private SwitchValidateService service; private SwitchValidateRequest request; private FlowSpeakerData flowSpeakerData; @@ -111,7 +112,7 @@ public void setUp() { when(repositoryFactory.createSwitchRepository()).thenReturn(switchRepository); when(persistenceManager.getRepositoryFactory()).thenReturn(repositoryFactory); - service = new SwitchValidateServiceImpl(carrier, persistenceManager, validationService, ruleManager); + service = new SwitchValidateService(carrier, persistenceManager, validationService, ruleManager); request = SwitchValidateRequest.builder().switchId(SWITCH_ID).processMeters(true).build(); flowSpeakerData = FlowSpeakerData.builder() @@ -144,21 +145,23 @@ public void smokeHandleRequest() { } @Test - public void receiveOnlyRules() { + public void receiveOnlyRules() throws UnexpectedInputException, MessageDispatchException { handleRequestAndInitDataReceive(); - service.handleFlowEntriesResponse(KEY, new FlowDumpResponse(SWITCH_ID, singletonList(flowSpeakerData))); + service.dispatchWorkerMessage( + new FlowDumpResponse(SWITCH_ID, singletonList(flowSpeakerData)), new MessageCookie(KEY)); verifyNoMoreInteractions(carrier); verifyNoMoreInteractions(validationService); } @Test - public void receiveTaskTimeout() { + public void receiveTaskTimeout() throws UnexpectedInputException, MessageDispatchException { handleRequestAndInitDataReceive(); - service.handleFlowEntriesResponse(KEY, new FlowDumpResponse(SWITCH_ID, singletonList(flowSpeakerData))); - service.handleTaskTimeout(KEY); + service.dispatchWorkerMessage( + new FlowDumpResponse(SWITCH_ID, singletonList(flowSpeakerData)), new MessageCookie(KEY)); + service.timeout(new MessageCookie(KEY)); verify(carrier).cancelTimeoutCallback(eq(KEY)); verify(carrier).errorResponse(eq(KEY), eq(ErrorType.OPERATION_TIMED_OUT), any(String.class), any(String.class)); @@ -167,12 +170,13 @@ public void receiveTaskTimeout() { } @Test - public void receiveTaskError() { + public void receiveTaskError() throws UnexpectedInputException, MessageDispatchException { handleRequestAndInitDataReceive(); - service.handleFlowEntriesResponse(KEY, new FlowDumpResponse(SWITCH_ID, singletonList(flowSpeakerData))); + service.dispatchWorkerMessage( + new FlowDumpResponse(SWITCH_ID, singletonList(flowSpeakerData)), new MessageCookie(KEY)); ErrorMessage errorMessage = getErrorMessage(); - service.handleTaskError(KEY, errorMessage); + service.dispatchWorkerMessage(errorMessage.getData(), new MessageCookie(KEY)); verify(carrier).cancelTimeoutCallback(eq(KEY)); verify(carrier).errorResponse(eq(KEY), eq(errorMessage.getData().getErrorType()), any(String.class), @@ -183,7 +187,7 @@ public void receiveTaskError() { } @Test - public void validationSuccess() { + public void validationSuccess() throws UnexpectedInputException, MessageDispatchException { handleRequestAndInitDataReceive(); handleDataReceiveAndValidate(); @@ -198,14 +202,15 @@ public void validationSuccess() { } @Test - public void validationWithoutMetersSuccess() { + public void validationWithoutMetersSuccess() throws UnexpectedInputException, MessageDispatchException { request = SwitchValidateRequest.builder().switchId(SWITCH_ID).build(); service.handleSwitchValidateRequest(KEY, request); verify(carrier, times(2)).sendCommandToSpeaker(eq(KEY), any(CommandData.class)); - service.handleFlowEntriesResponse(KEY, new FlowDumpResponse(SWITCH_ID, singletonList(flowSpeakerData))); - service.handleGroupEntriesResponse(KEY, new GroupDumpResponse(SWITCH_ID, emptyList())); + service.dispatchWorkerMessage( + new FlowDumpResponse(SWITCH_ID, singletonList(flowSpeakerData)), new MessageCookie(KEY)); + service.dispatchWorkerMessage(new GroupDumpResponse(SWITCH_ID, emptyList()), new MessageCookie(KEY)); verify(validationService).validateRules(eq(SWITCH_ID), any(), any()); verify(validationService).validateGroups(eq(SWITCH_ID), any(), any()); verify(carrier).cancelTimeoutCallback(eq(KEY)); @@ -221,11 +226,12 @@ public void validationWithoutMetersSuccess() { } @Test - public void validationSuccessWithUnsupportedMeters() { + public void validationSuccessWithUnsupportedMeters() throws UnexpectedInputException, MessageDispatchException { handleRequestAndInitDataReceive(); - service.handleFlowEntriesResponse(KEY, new FlowDumpResponse(SWITCH_ID, singletonList(flowSpeakerData))); - service.handleMetersUnsupportedResponse(KEY); - service.handleGroupEntriesResponse(KEY, new GroupDumpResponse(SWITCH_ID, emptyList())); + service.dispatchWorkerMessage( + new FlowDumpResponse(SWITCH_ID, singletonList(flowSpeakerData)), new MessageCookie(KEY)); + service.dispatchWorkerMessage(new SwitchMeterUnsupported(SWITCH_ID), new MessageCookie(KEY)); + service.dispatchWorkerMessage(new GroupDumpResponse(SWITCH_ID, emptyList()), new MessageCookie(KEY)); verify(validationService).validateRules(eq(SWITCH_ID), any(), any()); verify(validationService).validateGroups(eq(SWITCH_ID), any(), any()); @@ -242,7 +248,7 @@ public void validationSuccessWithUnsupportedMeters() { } @Test - public void exceptionWhileValidation() { + public void exceptionWhileValidation() throws UnexpectedInputException, MessageDispatchException { handleRequestAndInitDataReceive(); String errorMessage = "test error"; @@ -259,16 +265,14 @@ public void exceptionWhileValidation() { verifyNoMoreInteractions(validationService); } - @Test - public void doNothingWhenFsmNotFound() { - service.handleFlowEntriesResponse(KEY, new FlowDumpResponse(SWITCH_ID, singletonList(flowSpeakerData))); - - verifyZeroInteractions(carrier); - verifyZeroInteractions(validationService); + @Test(expected = MessageDispatchException.class) + public void doNothingWhenFsmNotFound() throws UnexpectedInputException, MessageDispatchException { + service.dispatchWorkerMessage( + new FlowDumpResponse(SWITCH_ID, singletonList(flowSpeakerData)), new MessageCookie(KEY)); } @Test - public void validationPerformSync() { + public void validationPerformSync() throws UnexpectedInputException, MessageDispatchException { request = SwitchValidateRequest.builder().switchId(SWITCH_ID).performSync(true).processMeters(true).build(); handleRequestAndInitDataReceive(); @@ -301,10 +305,12 @@ private void handleRequestAndInitDataReceive() { verifyNoMoreInteractions(carrier); } - private void handleDataReceiveAndValidate() { - service.handleFlowEntriesResponse(KEY, new FlowDumpResponse(SWITCH_ID, singletonList(flowSpeakerData))); - service.handleMeterEntriesResponse(KEY, new MeterDumpResponse(SWITCH_ID, singletonList(meterSpeakerData))); - service.handleGroupEntriesResponse(KEY, new GroupDumpResponse(SWITCH_ID, emptyList())); + private void handleDataReceiveAndValidate() throws UnexpectedInputException, MessageDispatchException { + service.dispatchWorkerMessage( + new FlowDumpResponse(SWITCH_ID, singletonList(flowSpeakerData)), new MessageCookie(KEY)); + service.dispatchWorkerMessage( + new MeterDumpResponse(SWITCH_ID, singletonList(meterSpeakerData)), new MessageCookie(KEY)); + service.dispatchWorkerMessage(new GroupDumpResponse(SWITCH_ID, emptyList()), new MessageCookie(KEY)); verify(validationService).validateRules(eq(SWITCH_ID), any(), any()); verify(validationService).validateMeters(eq(SWITCH_ID), any(), any()); From 7a0b485b72a330a5f492523e500b3245ffabc032 Mon Sep 17 00:00:00 2001 From: Dmitriy Bogun Date: Wed, 19 Jan 2022 20:41:09 +0200 Subject: [PATCH 07/11] LAG logical port update operation Add "update" operation to existing create / delete LAG port operations. --- docs/design/LAG-for-ports/README.md | 125 ++++++++++-- ... => CreateOrUpdateLogicalPortRequest.java} | 10 +- ...=> CreateOrUpdateLogicalPortResponse.java} | 8 +- .../controller/NoviflowController.java | 2 +- .../grpc/speaker/mapper/RequestMapper.java | 4 +- .../speaker/messaging/MessageProcessor.java | 22 +- .../speaker/service/GrpcSenderService.java | 2 +- .../repositories/PhysicalPortRepository.java | 3 +- .../ferma/frames/LagLogicalPortFrame.java | 21 +- .../FermaPhysicalPortRepository.java | 7 +- .../repositories/FermaPhysicalPortTest.java | 12 +- .../network/storm/bolt/GrpcRouter.java | 6 +- .../network/storm/bolt/bfd/hub/BfdHub.java | 4 +- .../BfdHubPortCreateResponseCommand.java | 7 +- .../storm/bolt/bfd/worker/BfdWorker.java | 9 +- .../bfd/worker/command/BfdWorkerCommand.java | 4 +- .../command/BfdWorkerPortCrudCommand.java | 4 +- .../BfdWorkerLogicalPortCreateResponse.java | 6 +- .../NetworkBfdLogicalPortServiceTest.java | 6 +- ...ateLagPortDto.java => LagPortRequest.java} | 2 +- .../{LagPortDto.java => LagPortResponse.java} | 2 +- .../controller/v2/SwitchControllerV2.java | 39 ++-- .../northbound/converter/LagPortMapper.java | 8 +- .../northbound/service/SwitchService.java | 13 +- .../service/impl/SwitchServiceImpl.java | 28 ++- .../converter/LagPortMapperTest.java | 4 +- .../request/DeleteLagPortRequest.java | 2 +- .../request/UpdateLagPortRequest.java | 32 +++ .../switchmanager/bolt/SwitchManagerHub.java | 7 + .../switchmanager/fsm/CreateLagPortFsm.java | 7 +- .../switchmanager/fsm/SwitchSyncFsm.java | 6 +- .../switchmanager/service/CommandBuilder.java | 4 +- .../service/CreateLagPortService.java | 8 +- .../service/LagPortOperationService.java | 134 ++++++++++--- .../service/SwitchSyncService.java | 4 +- .../service/UpdateLagPortService.java | 188 ++++++++++++++++++ .../service/handler/LagPortUpdateHandler.java | 187 +++++++++++++++++ .../service/impl/CommandBuilderImpl.java | 8 +- .../service/UpdateLagPortServiceTest.java | 91 +++++++++ .../handler/LagPortUpdateHandlerTest.java | 178 +++++++++++++++++ .../spec/flows/yflows/YFlowCreateSpec.groovy | 4 +- .../spec/server42/Server42FlowRttSpec.groovy | 4 +- .../spec/switches/LagPortSpec.groovy | 28 +-- .../northbound/NorthboundServiceV2.java | 10 +- .../northbound/NorthboundServiceV2Impl.java | 20 +- 45 files changed, 1093 insertions(+), 187 deletions(-) rename src-java/grpc-speaker/grpc-api/src/main/java/org/openkilda/messaging/command/grpc/{CreateLogicalPortRequest.java => CreateOrUpdateLogicalPortRequest.java} (74%) rename src-java/grpc-speaker/grpc-api/src/main/java/org/openkilda/messaging/info/grpc/{CreateLogicalPortResponse.java => CreateOrUpdateLogicalPortResponse.java} (78%) rename src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/switches/{CreateLagPortDto.java => LagPortRequest.java} (97%) rename src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/switches/{LagPortDto.java => LagPortResponse.java} (97%) create mode 100644 src-java/swmanager-topology/swmanager-messaging/src/main/java/org/openkilda/messaging/swmanager/request/UpdateLagPortRequest.java create mode 100644 src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/UpdateLagPortService.java create mode 100644 src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/handler/LagPortUpdateHandler.java create mode 100644 src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/UpdateLagPortServiceTest.java create mode 100644 src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/handler/LagPortUpdateHandlerTest.java diff --git a/docs/design/LAG-for-ports/README.md b/docs/design/LAG-for-ports/README.md index 45dee465a8c..0ea7da88cea 100644 --- a/docs/design/LAG-for-ports/README.md +++ b/docs/design/LAG-for-ports/README.md @@ -6,33 +6,130 @@ Link aggregation provides ability to combine multiple physical connections into ## API -* Get existing LAG logical ports on the switch `GET /v2/switches/{switch_id}/lags`. Response example: -~~~ -[ - { - "logical_port_number": 2001, - "port_numbers": [1, 2, 3] - }, -... -] +Shell variables required by API examples: +~~~shell +nb_host="localhost:8080" +switch_id="00:00:00:00:00:00:00:01" +auth_login="kilda" +auth_password="kilda" ~~~ -* Create LAG logical port on the switch `POST /v2/switches/{switch_id}/lags` with body: +### Create new LAG + +Request format: + +~~~shell +curl -X POST --location "http://${nb_host}/api/v2/switches/${switch_id}/lags" \ + -H "accept: */*" \ + -H "correlation_id: ${request_id:-dummy_correlation_id}" \ + -H "Content-Type: application/json" \ + --basic --user "${auth_login}:${auth_password}" \ + -d @- << POST_DATA +{ "port_numbers": [ 40, 41 ] } +POST_DATA ~~~ + +Response example: + +~~~json { - "port_numbers": [1, 2, 3] + "logical_port_number": 2891, + "port_numbers": [ + 40, + 41 + ] } ~~~ + + +### Read all LAGs on specific switch + +Request format: + +~~~shell +curl -X GET --location "http://${nb_host}/api/v2/switches/${switch_id}/lags" \ + -H "accept: */*" \ + -H "correlation_id: ${request_id:-dummy_correlation_id}" \ + --basic --user "${auth_login}:${auth_password}" +~~~ + Response example: + +~~~json +[ + { + "logical_port_number": 2891, + "port_numbers": [ + 40, + 41 + ] + }, + { + "logical_port_number": 2198, + "port_numbers": [ + 22, + 27 + ] + } +] ~~~ + + +### Update specific LAG on specific switch + +Request format: + +~~~shell +port_number=2198 + +curl -X PUT --location "http://${nb_host}/api/v2/switches/${switch_id}/lags/${port_number}" \ + -H "accept: */*" \ + -H "correlation_id: ${request_id:-dummy_correlation_id}" \ + -H "Content-Type: application/json" \ + --basic --user "${auth_login}:${auth_password}" \ + -d @- << PUT_DATA +{ "port_numbers": [ 35, 36 ] } +PUT_DATA +~~~ + +Response example: + +~~~json { - "logical_port_number": 2001, - "port_numbers": [1, 2, 3] + "logical_port_number": 2198, + "port_numbers": [ + 35, + 36 + ] } ~~~ -* Delete LAG logical port on the switch `DELETE /v2/switches/{switch_id}/lags/{logical_port_number}`. +### Delete specific LAG on specific switch + +Request format: + +~~~shell +port_number=2198 + +curl -X DELETE --location "http://${nb_host}/api/v2/switches/${switch_id}/lags/${port_number}" \ + -H "accept: */*" \ + -H "correlation_id: ${request_id:-dummy_correlation_id}" \ + -H "Content-Type: application/json" \ + --basic --user "${auth_login}:${auth_password}" +~~~ + +Response example: + +~~~json +{ + "logical_port_number": 2198, + "port_numbers": [ + 35, + 36 + ] +} +~~~ ## Details All logical port related commands are sent to the switches using gRPC speaker. diff --git a/src-java/grpc-speaker/grpc-api/src/main/java/org/openkilda/messaging/command/grpc/CreateLogicalPortRequest.java b/src-java/grpc-speaker/grpc-api/src/main/java/org/openkilda/messaging/command/grpc/CreateOrUpdateLogicalPortRequest.java similarity index 74% rename from src-java/grpc-speaker/grpc-api/src/main/java/org/openkilda/messaging/command/grpc/CreateLogicalPortRequest.java rename to src-java/grpc-speaker/grpc-api/src/main/java/org/openkilda/messaging/command/grpc/CreateOrUpdateLogicalPortRequest.java index 4450be3b00e..3d65c850386 100644 --- a/src-java/grpc-speaker/grpc-api/src/main/java/org/openkilda/messaging/command/grpc/CreateLogicalPortRequest.java +++ b/src-java/grpc-speaker/grpc-api/src/main/java/org/openkilda/messaging/command/grpc/CreateOrUpdateLogicalPortRequest.java @@ -26,7 +26,7 @@ @Data @EqualsAndHashCode(callSuper = true) -public class CreateLogicalPortRequest extends GrpcBaseRequest { +public class CreateOrUpdateLogicalPortRequest extends GrpcBaseRequest { @JsonProperty("port_numbers") private List portNumbers; @@ -38,10 +38,10 @@ public class CreateLogicalPortRequest extends GrpcBaseRequest { private LogicalPortType type; @JsonCreator - public CreateLogicalPortRequest(@JsonProperty("address") String address, - @JsonProperty("port_numbers") List portNumbers, - @JsonProperty("logical_port_number") int logicalPortNumber, - @JsonProperty("type") LogicalPortType type) { + public CreateOrUpdateLogicalPortRequest(@JsonProperty("address") String address, + @JsonProperty("port_numbers") List portNumbers, + @JsonProperty("logical_port_number") int logicalPortNumber, + @JsonProperty("type") LogicalPortType type) { super(address); this.portNumbers = portNumbers; this.logicalPortNumber = logicalPortNumber; diff --git a/src-java/grpc-speaker/grpc-api/src/main/java/org/openkilda/messaging/info/grpc/CreateLogicalPortResponse.java b/src-java/grpc-speaker/grpc-api/src/main/java/org/openkilda/messaging/info/grpc/CreateOrUpdateLogicalPortResponse.java similarity index 78% rename from src-java/grpc-speaker/grpc-api/src/main/java/org/openkilda/messaging/info/grpc/CreateLogicalPortResponse.java rename to src-java/grpc-speaker/grpc-api/src/main/java/org/openkilda/messaging/info/grpc/CreateOrUpdateLogicalPortResponse.java index a658947e16a..fe94ed4be9c 100644 --- a/src-java/grpc-speaker/grpc-api/src/main/java/org/openkilda/messaging/info/grpc/CreateLogicalPortResponse.java +++ b/src-java/grpc-speaker/grpc-api/src/main/java/org/openkilda/messaging/info/grpc/CreateOrUpdateLogicalPortResponse.java @@ -25,7 +25,7 @@ @Value @EqualsAndHashCode(callSuper = false) -public class CreateLogicalPortResponse extends InfoData { +public class CreateOrUpdateLogicalPortResponse extends InfoData { @JsonProperty("switch_address") private String switchAddress; @@ -37,9 +37,9 @@ public class CreateLogicalPortResponse extends InfoData { private boolean created; @JsonCreator - public CreateLogicalPortResponse(@JsonProperty("switch_address") String switchAddress, - @JsonProperty("logical_port") LogicalPort logicalPort, - @JsonProperty("created") boolean created) { + public CreateOrUpdateLogicalPortResponse(@JsonProperty("switch_address") String switchAddress, + @JsonProperty("logical_port") LogicalPort logicalPort, + @JsonProperty("created") boolean created) { this.switchAddress = switchAddress; this.logicalPort = logicalPort; this.created = created; diff --git a/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/controller/NoviflowController.java b/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/controller/NoviflowController.java index 26b7514a2a9..86584829faf 100644 --- a/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/controller/NoviflowController.java +++ b/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/controller/NoviflowController.java @@ -91,7 +91,7 @@ public CompletableFuture> getSwitchLogicalPorts( public CompletableFuture createLogicalPort( @PathVariable("switch_address") String switchAddress, @RequestBody LogicalPortDto logicalPortDto) { - return grpcService.createLogicalPort(switchAddress, logicalPortDto); + return grpcService.createOrUpdateLogicalPort(switchAddress, logicalPortDto); } @ApiOperation(value = "Get switch logical port configuration", response = LogicalPort.class) diff --git a/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/mapper/RequestMapper.java b/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/mapper/RequestMapper.java index 38e8e920316..a074e702607 100644 --- a/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/mapper/RequestMapper.java +++ b/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/mapper/RequestMapper.java @@ -16,12 +16,12 @@ package org.openkilda.grpc.speaker.mapper; import org.openkilda.grpc.speaker.model.LogicalPortDto; -import org.openkilda.messaging.command.grpc.CreateLogicalPortRequest; +import org.openkilda.messaging.command.grpc.CreateOrUpdateLogicalPortRequest; import org.mapstruct.Mapper; @Mapper(componentModel = "spring") public interface RequestMapper { - LogicalPortDto toLogicalPort(CreateLogicalPortRequest request); + LogicalPortDto toLogicalPort(CreateOrUpdateLogicalPortRequest request); } diff --git a/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/messaging/MessageProcessor.java b/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/messaging/MessageProcessor.java index c7c79205fe1..cafe6bb13a1 100644 --- a/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/messaging/MessageProcessor.java +++ b/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/messaging/MessageProcessor.java @@ -24,7 +24,7 @@ import org.openkilda.messaging.MessageData; import org.openkilda.messaging.command.CommandData; import org.openkilda.messaging.command.CommandMessage; -import org.openkilda.messaging.command.grpc.CreateLogicalPortRequest; +import org.openkilda.messaging.command.grpc.CreateOrUpdateLogicalPortRequest; import org.openkilda.messaging.command.grpc.DeleteLogicalPortRequest; import org.openkilda.messaging.command.grpc.DumpLogicalPortsRequest; import org.openkilda.messaging.command.grpc.GetPacketInOutStatsRequest; @@ -34,7 +34,7 @@ import org.openkilda.messaging.error.ErrorType; import org.openkilda.messaging.info.InfoData; import org.openkilda.messaging.info.InfoMessage; -import org.openkilda.messaging.info.grpc.CreateLogicalPortResponse; +import org.openkilda.messaging.info.grpc.CreateOrUpdateLogicalPortResponse; import org.openkilda.messaging.info.grpc.DeleteLogicalPortResponse; import org.openkilda.messaging.info.grpc.DumpLogicalPortsResponse; import org.openkilda.messaging.info.grpc.GetPacketInOutStatsResponse; @@ -88,16 +88,16 @@ private void handleCommandMessage(CommandMessage command, String key) { String correlationId = command.getCorrelationId(); CompletableFuture result; - if (data instanceof CreateLogicalPortRequest) { - result = handleCreateLogicalPortRequest((CreateLogicalPortRequest) data); + if (data instanceof CreateOrUpdateLogicalPortRequest) { + result = handleCreateOrUpdateLogicalPortRequest((CreateOrUpdateLogicalPortRequest) data); } else if (data instanceof DumpLogicalPortsRequest) { result = handleDumpLogicalPortsRequest((DumpLogicalPortsRequest) data); + } else if (data instanceof DeleteLogicalPortRequest) { + result = handleDeleteLogicalPortRequest((DeleteLogicalPortRequest) data); } else if (data instanceof GetSwitchInfoRequest) { result = handleGetSwitchInfoRequest((GetSwitchInfoRequest) data); } else if (data instanceof GetPacketInOutStatsRequest) { result = handleGetPacketInOutStatsRequest((GetPacketInOutStatsRequest) data); - } else if (data instanceof DeleteLogicalPortRequest) { - result = handleDeleteLogicalPortRequest((DeleteLogicalPortRequest) data); } else { result = unhandledMessage(command); } @@ -105,12 +105,14 @@ private void handleCommandMessage(CommandMessage command, String key) { result.thenAccept(response -> sendResponse(response, correlationId, key, command.getCookie())); } - private CompletableFuture handleCreateLogicalPortRequest(CreateLogicalPortRequest request) { + private CompletableFuture handleCreateOrUpdateLogicalPortRequest( + CreateOrUpdateLogicalPortRequest request) { CompletableFuture future = service - .createLogicalPort(request.getAddress(), requestMapper.toLogicalPort(request)) - .thenApply(result -> new CreateLogicalPortResponse(request.getAddress(), result, true)); + .createOrUpdateLogicalPort(request.getAddress(), requestMapper.toLogicalPort(request)) + .thenApply(result -> new CreateOrUpdateLogicalPortResponse(request.getAddress(), result, true)); return makeResponse(future, String.format( - "Creating logical port %s on switch %s", request.getLogicalPortNumber(), request.getAddress())); + "Creating or update %s logical port %s on switch %s", + request.getType(), request.getLogicalPortNumber(), request.getAddress())); } private CompletableFuture handleDumpLogicalPortsRequest(DumpLogicalPortsRequest request) { diff --git a/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/service/GrpcSenderService.java b/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/service/GrpcSenderService.java index c309d86b34c..2041cfbc474 100644 --- a/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/service/GrpcSenderService.java +++ b/src-java/grpc-speaker/grpc-service/src/main/java/org/openkilda/grpc/speaker/service/GrpcSenderService.java @@ -69,7 +69,7 @@ public GrpcSenderService(@Autowired NoviflowResponseMapper mapper) { * @param port the port data. * @return {@link CompletableFuture} with the execution result. */ - public CompletableFuture createLogicalPort(String switchAddress, LogicalPortDto port) { + public CompletableFuture createOrUpdateLogicalPort(String switchAddress, LogicalPortDto port) { try (GrpcSession session = makeSession(switchAddress)) { session.setLogicalPort(port); return session.showConfigLogicalPort(port.getLogicalPortNumber()) diff --git a/src-java/kilda-persistence-api/src/main/java/org/openkilda/persistence/repositories/PhysicalPortRepository.java b/src-java/kilda-persistence-api/src/main/java/org/openkilda/persistence/repositories/PhysicalPortRepository.java index 63462f7d127..661cf734554 100644 --- a/src-java/kilda-persistence-api/src/main/java/org/openkilda/persistence/repositories/PhysicalPortRepository.java +++ b/src-java/kilda-persistence-api/src/main/java/org/openkilda/persistence/repositories/PhysicalPortRepository.java @@ -20,12 +20,11 @@ import java.util.Collection; import java.util.Optional; -import java.util.Set; public interface PhysicalPortRepository extends Repository { Collection findAll(); - Set findPortNumbersBySwitchId(SwitchId switchId); + Collection findBySwitchId(SwitchId switchId); Optional findBySwitchIdAndPortNumber(SwitchId switchId, int portNumber); } diff --git a/src-java/kilda-persistence-tinkerpop/src/main/java/org/openkilda/persistence/ferma/frames/LagLogicalPortFrame.java b/src-java/kilda-persistence-tinkerpop/src/main/java/org/openkilda/persistence/ferma/frames/LagLogicalPortFrame.java index 4e7c7133fc8..17ae79996e0 100644 --- a/src-java/kilda-persistence-tinkerpop/src/main/java/org/openkilda/persistence/ferma/frames/LagLogicalPortFrame.java +++ b/src-java/kilda-persistence-tinkerpop/src/main/java/org/openkilda/persistence/ferma/frames/LagLogicalPortFrame.java @@ -27,10 +27,12 @@ import lombok.extern.slf4j.Slf4j; import org.apache.tinkerpop.gremlin.structure.Direction; import org.apache.tinkerpop.gremlin.structure.Edge; +import org.apache.tinkerpop.gremlin.structure.Vertex; import java.util.Comparator; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; @Slf4j @@ -75,13 +77,23 @@ public List getPhysicalPorts() { @Override public void setPhysicalPorts(List physicalPorts) { + Set missing = physicalPorts.stream() + .map(PhysicalPort::getPortNumber) + .collect(Collectors.toSet()); getElement().edges(Direction.OUT, COMPRISES_PHYSICAL_PORT_EDGE) - .forEachRemaining(edge -> { - edge.inVertex().remove(); - edge.remove(); - }); + .forEachRemaining(edge -> { + Vertex target = edge.inVertex(); + Integer value = target.value(PhysicalPortFrame.PORT_NUMBER_PROPERTY); + if (! missing.remove(value)) { + target.remove(); + } + }); for (PhysicalPort physicalPort : physicalPorts) { + if (! missing.contains(physicalPort.getPortNumber())) { + continue; + } + PhysicalPort.PhysicalPortData data = physicalPort.getData(); PhysicalPortFrame frame; @@ -90,6 +102,7 @@ public void setPhysicalPorts(List physicalPorts) { // Unlink physical port from the previous owner. frame.getElement().edges(Direction.IN, COMPRISES_PHYSICAL_PORT_EDGE) .forEachRemaining(Edge::remove); + frame.setSwitchId(getSwitchId()); // just precaution } else { frame = PhysicalPortFrame.create(getGraph(), data); } diff --git a/src-java/kilda-persistence-tinkerpop/src/main/java/org/openkilda/persistence/ferma/repositories/FermaPhysicalPortRepository.java b/src-java/kilda-persistence-tinkerpop/src/main/java/org/openkilda/persistence/ferma/repositories/FermaPhysicalPortRepository.java index e11856bfa83..95cc7d1931c 100644 --- a/src-java/kilda-persistence-tinkerpop/src/main/java/org/openkilda/persistence/ferma/repositories/FermaPhysicalPortRepository.java +++ b/src-java/kilda-persistence-tinkerpop/src/main/java/org/openkilda/persistence/ferma/repositories/FermaPhysicalPortRepository.java @@ -27,7 +27,6 @@ import java.util.Collection; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; /** @@ -50,14 +49,14 @@ public Collection findAll() { } @Override - public Set findPortNumbersBySwitchId(SwitchId switchId) { + public Collection findBySwitchId(SwitchId switchId) { return framedGraph().traverse(g -> g.V() .hasLabel(PhysicalPortFrame.FRAME_LABEL) .has(PhysicalPortFrame.SWITCH_ID_PROPERTY, SwitchIdConverter.INSTANCE.toGraphProperty(switchId))) .toListExplicit(PhysicalPortFrame.class).stream() - .map(PhysicalPortFrame::getPortNumber) - .collect(Collectors.toSet()); + .map(PhysicalPort::new) + .collect(Collectors.toList()); } @Override diff --git a/src-java/kilda-persistence-tinkerpop/src/test/java/org/openkilda/persistence/ferma/repositories/FermaPhysicalPortTest.java b/src-java/kilda-persistence-tinkerpop/src/test/java/org/openkilda/persistence/ferma/repositories/FermaPhysicalPortTest.java index e6f2f71e341..4bab45e560d 100644 --- a/src-java/kilda-persistence-tinkerpop/src/test/java/org/openkilda/persistence/ferma/repositories/FermaPhysicalPortTest.java +++ b/src-java/kilda-persistence-tinkerpop/src/test/java/org/openkilda/persistence/ferma/repositories/FermaPhysicalPortTest.java @@ -29,11 +29,13 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.mapstruct.ap.internal.util.Collections; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; public class FermaPhysicalPortTest extends InMemoryGraphBasedTest { static final SwitchId SWITCH_ID_1 = new SwitchId(1); @@ -109,12 +111,12 @@ public void findPortNumbersBySwitchIdTest() { createPhysicalPort(SWITCH_ID_1, PHYSICAL_PORT_NUMBER_2, logicalPort1); createPhysicalPort(SWITCH_ID_2, PHYSICAL_PORT_NUMBER_3, logicalPort3); - Set portNumbers = physicalPortRepository.findPortNumbersBySwitchId(SWITCH_ID_1); - assertEquals(2, portNumbers.size()); - assertTrue(portNumbers.contains(PHYSICAL_PORT_NUMBER_1)); - assertTrue(portNumbers.contains(PHYSICAL_PORT_NUMBER_2)); + Set portNumbers = physicalPortRepository.findBySwitchId(SWITCH_ID_1).stream() + .map(PhysicalPort::getPortNumber) + .collect(Collectors.toSet()); + assertEquals(Collections.asSet(PHYSICAL_PORT_NUMBER_1, PHYSICAL_PORT_NUMBER_2), portNumbers); - assertTrue(physicalPortRepository.findPortNumbersBySwitchId(SWITCH_ID_3).isEmpty()); + assertTrue(physicalPortRepository.findBySwitchId(SWITCH_ID_3).isEmpty()); } private LagLogicalPort createLogicalPort(SwitchId switchId, int logicalPortNumber) { diff --git a/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/GrpcRouter.java b/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/GrpcRouter.java index 1cf8260c785..a121f3389d7 100644 --- a/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/GrpcRouter.java +++ b/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/GrpcRouter.java @@ -20,7 +20,7 @@ import org.openkilda.messaging.error.ErrorMessage; import org.openkilda.messaging.info.InfoData; import org.openkilda.messaging.info.InfoMessage; -import org.openkilda.messaging.info.grpc.CreateLogicalPortResponse; +import org.openkilda.messaging.info.grpc.CreateOrUpdateLogicalPortResponse; import org.openkilda.messaging.info.grpc.DeleteLogicalPortResponse; import org.openkilda.wfm.AbstractBolt; import org.openkilda.wfm.error.PipelineException; @@ -68,9 +68,9 @@ private void route(Tuple input, Message message) throws PipelineException { private void route(Tuple input, InfoMessage message) throws PipelineException { InfoData payload = message.getData(); String key = pullKey(input); - if (payload instanceof CreateLogicalPortResponse) { + if (payload instanceof CreateOrUpdateLogicalPortResponse) { emit(STREAM_BFD_WORKER_ID, input, makeBfdWorkerTuple( - key, new BfdWorkerLogicalPortCreateResponse((CreateLogicalPortResponse) payload))); + key, new BfdWorkerLogicalPortCreateResponse((CreateOrUpdateLogicalPortResponse) payload))); } else if (payload instanceof DeleteLogicalPortResponse) { emit(STREAM_BFD_WORKER_ID, input, makeBfdWorkerTuple( key, new BfdWorkerLogicalPortDeleteResponse((DeleteLogicalPortResponse) payload))); diff --git a/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/hub/BfdHub.java b/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/hub/BfdHub.java index be26c56925e..8d2b18113b2 100644 --- a/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/hub/BfdHub.java +++ b/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/hub/BfdHub.java @@ -17,7 +17,7 @@ import org.openkilda.messaging.error.ErrorData; import org.openkilda.messaging.floodlight.response.BfdSessionResponse; -import org.openkilda.messaging.info.grpc.CreateLogicalPortResponse; +import org.openkilda.messaging.info.grpc.CreateOrUpdateLogicalPortResponse; import org.openkilda.messaging.info.grpc.DeleteLogicalPortResponse; import org.openkilda.messaging.model.NoviBfdSession; import org.openkilda.model.BfdProperties; @@ -287,7 +287,7 @@ public void processOnlineStatusUpdate(SwitchId switchId, boolean isOnline) { } public void processLogicalPortCreateResponse( - String requestId, Endpoint logical, CreateLogicalPortResponse response) { + String requestId, Endpoint logical, CreateOrUpdateLogicalPortResponse response) { logicalPortService.workerSuccess(requestId, logical, response); } diff --git a/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/hub/command/BfdHubPortCreateResponseCommand.java b/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/hub/command/BfdHubPortCreateResponseCommand.java index 76303c431df..1660e3cfac0 100644 --- a/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/hub/command/BfdHubPortCreateResponseCommand.java +++ b/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/hub/command/BfdHubPortCreateResponseCommand.java @@ -15,16 +15,17 @@ package org.openkilda.wfm.topology.network.storm.bolt.bfd.hub.command; -import org.openkilda.messaging.info.grpc.CreateLogicalPortResponse; +import org.openkilda.messaging.info.grpc.CreateOrUpdateLogicalPortResponse; import org.openkilda.wfm.share.model.Endpoint; import org.openkilda.wfm.topology.network.storm.bolt.bfd.hub.BfdHub; public class BfdHubPortCreateResponseCommand extends BfdHubPortCommand { private final String requestId; - private final CreateLogicalPortResponse response; + private final CreateOrUpdateLogicalPortResponse response; - public BfdHubPortCreateResponseCommand(String requestId, Endpoint endpoint, CreateLogicalPortResponse response) { + public BfdHubPortCreateResponseCommand( + String requestId, Endpoint endpoint, CreateOrUpdateLogicalPortResponse response) { super(endpoint); this.requestId = requestId; this.response = response; diff --git a/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/worker/BfdWorker.java b/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/worker/BfdWorker.java index c7a51ab0ccb..55b47ed6206 100644 --- a/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/worker/BfdWorker.java +++ b/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/worker/BfdWorker.java @@ -16,14 +16,14 @@ package org.openkilda.wfm.topology.network.storm.bolt.bfd.worker; import org.openkilda.messaging.command.CommandData; -import org.openkilda.messaging.command.grpc.CreateLogicalPortRequest; +import org.openkilda.messaging.command.grpc.CreateOrUpdateLogicalPortRequest; import org.openkilda.messaging.command.grpc.DeleteLogicalPortRequest; import org.openkilda.messaging.error.ErrorData; import org.openkilda.messaging.error.ErrorType; import org.openkilda.messaging.floodlight.request.RemoveBfdSession; import org.openkilda.messaging.floodlight.request.SetupBfdSession; import org.openkilda.messaging.floodlight.response.BfdSessionResponse; -import org.openkilda.messaging.info.grpc.CreateLogicalPortResponse; +import org.openkilda.messaging.info.grpc.CreateOrUpdateLogicalPortResponse; import org.openkilda.messaging.info.grpc.DeleteLogicalPortResponse; import org.openkilda.messaging.model.NoviBfdSession; import org.openkilda.messaging.model.grpc.LogicalPortType; @@ -149,7 +149,7 @@ public void processPortCreateRequest(String requestId, Endpoint logical, int phy return; } - CreateLogicalPortRequest request = new CreateLogicalPortRequest( + CreateOrUpdateLogicalPortRequest request = new CreateOrUpdateLogicalPortRequest( address.get(), Collections.singletonList(physicalPortNumber), logical.getPortNumber(), LogicalPortType.BFD); emit(STREAM_GRPC_ID, getCurrentTuple(), makeGrpcTuple(requestId, request)); @@ -169,7 +169,8 @@ public void processPortDeleteRequest(String requestId, Endpoint logical) { emit(STREAM_GRPC_ID, getCurrentTuple(), makeGrpcTuple(requestId, request)); } - public void processPortCreateResponse(String requestId, Endpoint logical, CreateLogicalPortResponse response) { + public void processPortCreateResponse( + String requestId, Endpoint logical, CreateOrUpdateLogicalPortResponse response) { emitResponseToHub(getCurrentTuple(), makeHubTuple(requestId, new BfdHubPortCreateResponseCommand( requestId, logical, response))); } diff --git a/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/worker/command/BfdWorkerCommand.java b/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/worker/command/BfdWorkerCommand.java index d1464ce5096..fd7359fa32b 100644 --- a/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/worker/command/BfdWorkerCommand.java +++ b/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/worker/command/BfdWorkerCommand.java @@ -18,7 +18,7 @@ import org.openkilda.messaging.MessageData; import org.openkilda.messaging.error.ErrorData; import org.openkilda.messaging.floodlight.response.BfdSessionResponse; -import org.openkilda.messaging.info.grpc.CreateLogicalPortResponse; +import org.openkilda.messaging.info.grpc.CreateOrUpdateLogicalPortResponse; import org.openkilda.messaging.info.grpc.DeleteLogicalPortResponse; import org.openkilda.wfm.topology.network.storm.ICommand; import org.openkilda.wfm.topology.network.storm.bolt.bfd.worker.BfdWorker; @@ -37,7 +37,7 @@ public void consumeResponse(BfdWorker handler, BfdSessionResponse response) { failedToConsume(response); } - public void consumeResponse(BfdWorker handler, CreateLogicalPortResponse response) { + public void consumeResponse(BfdWorker handler, CreateOrUpdateLogicalPortResponse response) { failedToConsume(response); } diff --git a/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/worker/command/BfdWorkerPortCrudCommand.java b/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/worker/command/BfdWorkerPortCrudCommand.java index d9f7344b905..e97562ca0dc 100644 --- a/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/worker/command/BfdWorkerPortCrudCommand.java +++ b/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/worker/command/BfdWorkerPortCrudCommand.java @@ -16,7 +16,7 @@ package org.openkilda.wfm.topology.network.storm.bolt.bfd.worker.command; import org.openkilda.messaging.error.ErrorData; -import org.openkilda.messaging.info.grpc.CreateLogicalPortResponse; +import org.openkilda.messaging.info.grpc.CreateOrUpdateLogicalPortResponse; import org.openkilda.messaging.info.grpc.DeleteLogicalPortResponse; import org.openkilda.wfm.share.model.Endpoint; import org.openkilda.wfm.topology.network.storm.bolt.bfd.worker.BfdWorker; @@ -31,7 +31,7 @@ public BfdWorkerPortCrudCommand(String requestId, Endpoint logical) { } @Override - public void consumeResponse(BfdWorker handler, CreateLogicalPortResponse response) { + public void consumeResponse(BfdWorker handler, CreateOrUpdateLogicalPortResponse response) { handler.processPortCreateResponse(getRequestId(), logical, response); } diff --git a/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/worker/response/BfdWorkerLogicalPortCreateResponse.java b/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/worker/response/BfdWorkerLogicalPortCreateResponse.java index db2687ba618..c553e153ce7 100644 --- a/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/worker/response/BfdWorkerLogicalPortCreateResponse.java +++ b/src-java/network-topology/network-storm-topology/src/main/java/org/openkilda/wfm/topology/network/storm/bolt/bfd/worker/response/BfdWorkerLogicalPortCreateResponse.java @@ -15,14 +15,14 @@ package org.openkilda.wfm.topology.network.storm.bolt.bfd.worker.response; -import org.openkilda.messaging.info.grpc.CreateLogicalPortResponse; +import org.openkilda.messaging.info.grpc.CreateOrUpdateLogicalPortResponse; import org.openkilda.wfm.topology.network.storm.bolt.bfd.worker.BfdWorker; import org.openkilda.wfm.topology.network.storm.bolt.bfd.worker.command.BfdWorkerCommand; public class BfdWorkerLogicalPortCreateResponse extends BfdWorkerAsyncResponse { - private final CreateLogicalPortResponse response; + private final CreateOrUpdateLogicalPortResponse response; - public BfdWorkerLogicalPortCreateResponse(CreateLogicalPortResponse response) { + public BfdWorkerLogicalPortCreateResponse(CreateOrUpdateLogicalPortResponse response) { this.response = response; } diff --git a/src-java/network-topology/network-storm-topology/src/test/java/org/openkilda/wfm/topology/network/service/NetworkBfdLogicalPortServiceTest.java b/src-java/network-topology/network-storm-topology/src/test/java/org/openkilda/wfm/topology/network/service/NetworkBfdLogicalPortServiceTest.java index 229bce0173f..ba6e2d6f3a7 100644 --- a/src-java/network-topology/network-storm-topology/src/test/java/org/openkilda/wfm/topology/network/service/NetworkBfdLogicalPortServiceTest.java +++ b/src-java/network-topology/network-storm-topology/src/test/java/org/openkilda/wfm/topology/network/service/NetworkBfdLogicalPortServiceTest.java @@ -22,7 +22,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import org.openkilda.messaging.info.grpc.CreateLogicalPortResponse; +import org.openkilda.messaging.info.grpc.CreateOrUpdateLogicalPortResponse; import org.openkilda.messaging.info.grpc.DeleteLogicalPortResponse; import org.openkilda.messaging.model.grpc.LogicalPort; import org.openkilda.messaging.model.grpc.LogicalPortType; @@ -350,8 +350,8 @@ private NetworkBfdLogicalPortService makeService() { return new NetworkBfdLogicalPortService(carrier, switchOnlineStatusMonitor, LOGICAL_PORT_OFFSET); } - private CreateLogicalPortResponse makePortCreateResponse(Endpoint physical, Endpoint logical) { - return new CreateLogicalPortResponse( + private CreateOrUpdateLogicalPortResponse makePortCreateResponse(Endpoint physical, Endpoint logical) { + return new CreateOrUpdateLogicalPortResponse( "127.0.1.1", LogicalPort.builder() .portNumber(physical.getPortNumber()) diff --git a/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/switches/CreateLagPortDto.java b/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/switches/LagPortRequest.java similarity index 97% rename from src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/switches/CreateLagPortDto.java rename to src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/switches/LagPortRequest.java index 35dcf53d4d8..c4110321eed 100644 --- a/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/switches/CreateLagPortDto.java +++ b/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/switches/LagPortRequest.java @@ -27,6 +27,6 @@ @NoArgsConstructor @AllArgsConstructor @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) -public class CreateLagPortDto { +public class LagPortRequest { private List portNumbers; } diff --git a/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/switches/LagPortDto.java b/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/switches/LagPortResponse.java similarity index 97% rename from src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/switches/LagPortDto.java rename to src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/switches/LagPortResponse.java index 90a05f1e922..d85e63a31fc 100644 --- a/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/switches/LagPortDto.java +++ b/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/switches/LagPortResponse.java @@ -27,7 +27,7 @@ @NoArgsConstructor @AllArgsConstructor @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) -public class LagPortDto { +public class LagPortResponse { private int logicalPortNumber; private List portNumbers; } diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/SwitchControllerV2.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/SwitchControllerV2.java index 28a87ec2994..18eca9b02a6 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/SwitchControllerV2.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/SwitchControllerV2.java @@ -19,8 +19,8 @@ import org.openkilda.messaging.error.MessageException; import org.openkilda.model.SwitchId; import org.openkilda.northbound.controller.BaseController; -import org.openkilda.northbound.dto.v2.switches.CreateLagPortDto; -import org.openkilda.northbound.dto.v2.switches.LagPortDto; +import org.openkilda.northbound.dto.v2.switches.LagPortRequest; +import org.openkilda.northbound.dto.v2.switches.LagPortResponse; import org.openkilda.northbound.dto.v2.switches.PortHistoryResponse; import org.openkilda.northbound.dto.v2.switches.PortPropertiesDto; import org.openkilda.northbound.dto.v2.switches.PortPropertiesResponse; @@ -192,13 +192,14 @@ public CompletableFuture getSwitchProperties() { * * @param switchId the switch */ - @ApiOperation(value = "Create LAG logical port", response = LagPortDto.class) + @ApiOperation(value = "Create LAG logical port", response = LagPortResponse.class) @PostMapping(value = "/{switch_id}/lags") @ResponseStatus(HttpStatus.OK) - public CompletableFuture createLagPort(@PathVariable("switch_id") SwitchId switchId, - @ApiParam(value = "Physical ports which will be grouped") - @RequestBody CreateLagPortDto createLagPortDto) { - return switchService.createLag(switchId, createLagPortDto); + public CompletableFuture createLagPort( + @PathVariable("switch_id") SwitchId switchId, + @ApiParam(value = "Physical ports which will be grouped") + @RequestBody LagPortRequest lagPortRequest) { + return switchService.createLag(switchId, lagPortRequest); } /** @@ -206,24 +207,38 @@ public CompletableFuture createLagPort(@PathVariable("switch_id") Sw * * @param switchId the switch */ - @ApiOperation(value = "Get LAG logical ports", response = LagPortDto.class) + @ApiOperation(value = "Read all LAG logical ports on specific switch", response = LagPortResponse.class) @GetMapping(value = "/{switch_id}/lags") @ResponseStatus(HttpStatus.OK) - public CompletableFuture> getLagPorts(@PathVariable("switch_id") SwitchId switchId) { + public CompletableFuture> getLagPorts(@PathVariable("switch_id") SwitchId switchId) { return switchService.getLagPorts(switchId); } + /** + * Update LAG logical port. + */ + @ApiOperation(value = "Update LAG logical port", response = LagPortResponse.class) + @PutMapping(value = "/{switch_id}/lags/{logical_port_number}") + @ResponseStatus(HttpStatus.OK) + public CompletableFuture updateLagPort( + @PathVariable("switch_id") SwitchId switchId, + @PathVariable("logical_port_number") int logicalPortNumber, + @RequestBody LagPortRequest payload) { + return switchService.updateLagPort(switchId, logicalPortNumber, payload); + } + /** * Delete LAG logical port. * * @param switchId the switch * @param logicalPortNumber the switch */ - @ApiOperation(value = "Delete LAG logical port", response = LagPortDto.class) + @ApiOperation(value = "Delete LAG logical port", response = LagPortResponse.class) @DeleteMapping(value = "/{switch_id}/lags/{logical_port_number}") @ResponseStatus(HttpStatus.OK) - public CompletableFuture deleteLagPort(@PathVariable("switch_id") SwitchId switchId, - @PathVariable("logical_port_number") Integer logicalPortNumber) { + public CompletableFuture deleteLagPort( + @PathVariable("switch_id") SwitchId switchId, + @PathVariable("logical_port_number") int logicalPortNumber) { return switchService.deleteLagPort(switchId, logicalPortNumber); } } diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/converter/LagPortMapper.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/converter/LagPortMapper.java index c6e2a1a4c0e..7e863ed896f 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/converter/LagPortMapper.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/converter/LagPortMapper.java @@ -15,14 +15,14 @@ package org.openkilda.northbound.converter; -import org.openkilda.messaging.swmanager.response.LagPortResponse; -import org.openkilda.northbound.dto.v2.switches.LagPortDto; +import org.openkilda.messaging.nbtopology.response.LagPortDto; +import org.openkilda.northbound.dto.v2.switches.LagPortResponse; import org.mapstruct.Mapper; @Mapper(componentModel = "spring") public interface LagPortMapper { - LagPortDto map(LagPortResponse response); + LagPortResponse map(org.openkilda.messaging.swmanager.response.LagPortResponse response); - LagPortDto map(org.openkilda.messaging.nbtopology.response.LagPortDto response); + LagPortResponse map(LagPortDto response); } diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/SwitchService.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/SwitchService.java index 1b3af02eada..5022fd8c761 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/SwitchService.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/SwitchService.java @@ -36,8 +36,8 @@ import org.openkilda.northbound.dto.v1.switches.SwitchSyncResult; import org.openkilda.northbound.dto.v1.switches.SwitchValidationResult; import org.openkilda.northbound.dto.v1.switches.UnderMaintenanceDto; -import org.openkilda.northbound.dto.v2.switches.CreateLagPortDto; -import org.openkilda.northbound.dto.v2.switches.LagPortDto; +import org.openkilda.northbound.dto.v2.switches.LagPortRequest; +import org.openkilda.northbound.dto.v2.switches.LagPortResponse; import org.openkilda.northbound.dto.v2.switches.PortHistoryResponse; import org.openkilda.northbound.dto.v2.switches.PortPropertiesDto; import org.openkilda.northbound.dto.v2.switches.PortPropertiesResponse; @@ -301,9 +301,12 @@ CompletableFuture updatePortProperties(SwitchId switchId CompletableFuture getSwitchConnections(SwitchId switchId); - CompletableFuture createLag(SwitchId switchId, CreateLagPortDto createLagPortDto); + CompletableFuture createLag(SwitchId switchId, LagPortRequest lagPortRequest); - CompletableFuture> getLagPorts(SwitchId switchId); + CompletableFuture> getLagPorts(SwitchId switchId); - CompletableFuture deleteLagPort(SwitchId switchId, Integer logicalPortNumber); + CompletableFuture updateLagPort( + SwitchId switchId, int logicalPortNumber, LagPortRequest payload); + + CompletableFuture deleteLagPort(SwitchId switchId, int logicalPortNumber); } diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/SwitchServiceImpl.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/SwitchServiceImpl.java index 887881a308e..bf37ee9c16d 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/SwitchServiceImpl.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/SwitchServiceImpl.java @@ -74,7 +74,7 @@ import org.openkilda.messaging.payload.switches.PortPropertiesPayload; import org.openkilda.messaging.swmanager.request.CreateLagPortRequest; import org.openkilda.messaging.swmanager.request.DeleteLagPortRequest; -import org.openkilda.messaging.swmanager.response.LagPortResponse; +import org.openkilda.messaging.swmanager.request.UpdateLagPortRequest; import org.openkilda.model.MacAddress; import org.openkilda.model.PortStatus; import org.openkilda.model.SwitchId; @@ -93,8 +93,8 @@ import org.openkilda.northbound.dto.v1.switches.SwitchSyncResult; import org.openkilda.northbound.dto.v1.switches.SwitchValidationResult; import org.openkilda.northbound.dto.v1.switches.UnderMaintenanceDto; -import org.openkilda.northbound.dto.v2.switches.CreateLagPortDto; -import org.openkilda.northbound.dto.v2.switches.LagPortDto; +import org.openkilda.northbound.dto.v2.switches.LagPortRequest; +import org.openkilda.northbound.dto.v2.switches.LagPortResponse; import org.openkilda.northbound.dto.v2.switches.PortHistoryResponse; import org.openkilda.northbound.dto.v2.switches.PortPropertiesDto; import org.openkilda.northbound.dto.v2.switches.PortPropertiesResponse; @@ -604,19 +604,19 @@ public CompletableFuture getSwitchConnections(SwitchI } @Override - public CompletableFuture createLag(SwitchId switchId, CreateLagPortDto lagPortDto) { + public CompletableFuture createLag(SwitchId switchId, LagPortRequest lagPortDto) { logger.info("Create Link aggregation group on switch {}, ports {}", switchId, lagPortDto.getPortNumbers()); CreateLagPortRequest data = new CreateLagPortRequest(switchId, lagPortDto.getPortNumbers()); CommandMessage request = new CommandMessage(data, System.currentTimeMillis(), RequestCorrelationId.getId()); return messagingChannel.sendAndGet(switchManagerTopic, request) - .thenApply(LagPortResponse.class::cast) + .thenApply(org.openkilda.messaging.swmanager.response.LagPortResponse.class::cast) .thenApply(lagPortMapper::map); } @Override - public CompletableFuture> getLagPorts(SwitchId switchId) { + public CompletableFuture> getLagPorts(SwitchId switchId) { logger.info("Getting Link aggregation groups on switch {}", switchId); GetSwitchLagPortsRequest data = new GetSwitchLagPortsRequest(switchId); @@ -631,14 +631,26 @@ public CompletableFuture> getLagPorts(SwitchId switchId) { } @Override - public CompletableFuture deleteLagPort(SwitchId switchId, Integer logicalPortNumber) { + public CompletableFuture updateLagPort( + SwitchId switchId, int logicalPortNumber, LagPortRequest payload) { + logger.info("Updating LAG logical port {} on {} with {}", logicalPortNumber, switchId, payload); + + UpdateLagPortRequest request = new UpdateLagPortRequest(switchId, logicalPortNumber, payload.getPortNumbers()); + CommandMessage message = new CommandMessage(request, System.currentTimeMillis(), RequestCorrelationId.getId()); + return messagingChannel.sendAndGet(switchManagerTopic, message) + .thenApply(org.openkilda.messaging.swmanager.response.LagPortResponse.class::cast) + .thenApply(lagPortMapper::map); + } + + @Override + public CompletableFuture deleteLagPort(SwitchId switchId, int logicalPortNumber) { logger.info("Removing Link aggregation group {} on switch {}", logicalPortNumber, switchId); DeleteLagPortRequest data = new DeleteLagPortRequest(switchId, logicalPortNumber); CommandMessage request = new CommandMessage(data, System.currentTimeMillis(), RequestCorrelationId.getId()); return messagingChannel.sendAndGet(switchManagerTopic, request) - .thenApply(LagPortResponse.class::cast) + .thenApply(org.openkilda.messaging.swmanager.response.LagPortResponse.class::cast) .thenApply(lagPortMapper::map); } diff --git a/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/converter/LagPortMapperTest.java b/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/converter/LagPortMapperTest.java index 1a3b636c48b..30f90fed59e 100644 --- a/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/converter/LagPortMapperTest.java +++ b/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/converter/LagPortMapperTest.java @@ -42,7 +42,7 @@ public void mapLagPortDtoTest() { LagPortDto response = new LagPortDto(LOGICAL_PORT_NUMBER_1, Lists.newArrayList(PHYSICAL_PORT_NUMBER_1, PHYSICAL_PORT_NUMBER_2)); - org.openkilda.northbound.dto.v2.switches.LagPortDto dto = lagMapper.map(response); + org.openkilda.northbound.dto.v2.switches.LagPortResponse dto = lagMapper.map(response); assertEquals(LOGICAL_PORT_NUMBER_1, dto.getLogicalPortNumber()); assertEquals(PHYSICAL_PORT_NUMBER_1, dto.getPortNumbers().get(0).intValue()); assertEquals(PHYSICAL_PORT_NUMBER_2, dto.getPortNumbers().get(1).intValue()); @@ -53,7 +53,7 @@ public void mapLagResponseTest() { LagPortResponse response = new LagPortResponse(LOGICAL_PORT_NUMBER_1, Lists.newArrayList(PHYSICAL_PORT_NUMBER_1, PHYSICAL_PORT_NUMBER_2)); - org.openkilda.northbound.dto.v2.switches.LagPortDto dto = lagMapper.map(response); + org.openkilda.northbound.dto.v2.switches.LagPortResponse dto = lagMapper.map(response); assertEquals(LOGICAL_PORT_NUMBER_1, dto.getLogicalPortNumber()); assertEquals(PHYSICAL_PORT_NUMBER_1, dto.getPortNumbers().get(0).intValue()); assertEquals(PHYSICAL_PORT_NUMBER_2, dto.getPortNumbers().get(1).intValue()); diff --git a/src-java/swmanager-topology/swmanager-messaging/src/main/java/org/openkilda/messaging/swmanager/request/DeleteLagPortRequest.java b/src-java/swmanager-topology/swmanager-messaging/src/main/java/org/openkilda/messaging/swmanager/request/DeleteLagPortRequest.java index 13314d1458a..9a77447b78c 100644 --- a/src-java/swmanager-topology/swmanager-messaging/src/main/java/org/openkilda/messaging/swmanager/request/DeleteLagPortRequest.java +++ b/src-java/swmanager-topology/swmanager-messaging/src/main/java/org/openkilda/messaging/swmanager/request/DeleteLagPortRequest.java @@ -25,6 +25,6 @@ @EqualsAndHashCode(callSuper = false) public class DeleteLagPortRequest extends CommandData { SwitchId switchId; - Integer logicalPortNumber; + int logicalPortNumber; } diff --git a/src-java/swmanager-topology/swmanager-messaging/src/main/java/org/openkilda/messaging/swmanager/request/UpdateLagPortRequest.java b/src-java/swmanager-topology/swmanager-messaging/src/main/java/org/openkilda/messaging/swmanager/request/UpdateLagPortRequest.java new file mode 100644 index 00000000000..3a28c559d08 --- /dev/null +++ b/src-java/swmanager-topology/swmanager-messaging/src/main/java/org/openkilda/messaging/swmanager/request/UpdateLagPortRequest.java @@ -0,0 +1,32 @@ +/* Copyright 2022 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.swmanager.request; + +import org.openkilda.messaging.command.CommandData; +import org.openkilda.model.SwitchId; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +import java.util.List; + +@Value +@EqualsAndHashCode(callSuper = false) +public class UpdateLagPortRequest extends CommandData { + SwitchId switchId; + int logicalPortNumber; + List targetPorts; +} diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/bolt/SwitchManagerHub.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/bolt/SwitchManagerHub.java index fd2fac89e8c..6637527569f 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/bolt/SwitchManagerHub.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/bolt/SwitchManagerHub.java @@ -30,6 +30,7 @@ import org.openkilda.messaging.info.InfoMessage; import org.openkilda.messaging.swmanager.request.CreateLagPortRequest; import org.openkilda.messaging.swmanager.request.DeleteLagPortRequest; +import org.openkilda.messaging.swmanager.request.UpdateLagPortRequest; import org.openkilda.persistence.PersistenceManager; import org.openkilda.rulemanager.RuleManagerConfig; import org.openkilda.rulemanager.RuleManagerImpl; @@ -54,6 +55,7 @@ import org.openkilda.wfm.topology.switchmanager.service.SwitchRuleService; import org.openkilda.wfm.topology.switchmanager.service.SwitchSyncService; import org.openkilda.wfm.topology.switchmanager.service.SwitchValidateService; +import org.openkilda.wfm.topology.switchmanager.service.UpdateLagPortService; import org.openkilda.wfm.topology.switchmanager.service.impl.ValidationServiceImpl; import org.openkilda.wfm.topology.utils.MessageKafkaTranslator; @@ -98,6 +100,7 @@ public class SwitchManagerHub extends HubBolt implements SwitchManagerCarrier { private transient SwitchSyncService syncService; private transient SwitchRuleService switchRuleService; private transient CreateLagPortService createLagPortService; + private transient UpdateLagPortService updateLagPortService; private transient DeleteLagPortService deleteLagPortService; private transient Map timeoutDispatchMap; @@ -147,6 +150,8 @@ public void init() { carrier -> new SwitchRuleService(carrier, persistenceManager.getRepositoryFactory())); createLagPortService = registerService( serviceRegistry, "lag-create", this, carrier -> new CreateLagPortService(carrier, config)); + updateLagPortService = registerService( + serviceRegistry, "lag-update", this, carrier -> new UpdateLagPortService(carrier, config)); deleteLagPortService = registerService( serviceRegistry, "lag-delete", this, carrier -> new DeleteLagPortService(carrier, config)); } @@ -211,6 +216,8 @@ private boolean dispatchRequest(String key, CommandData data) { dispatchRequest( createLagPortService, key, service -> service.handleCreateLagRequest(key, (CreateLagPortRequest) data)); + } else if (data instanceof UpdateLagPortRequest) { + dispatchRequest(updateLagPortService, key, service -> service.update(key, (UpdateLagPortRequest) data)); } else if (data instanceof DeleteLagPortRequest) { dispatchRequest( deleteLagPortService, key, diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/fsm/CreateLagPortFsm.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/fsm/CreateLagPortFsm.java index f4297812747..63a09f4a3e9 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/fsm/CreateLagPortFsm.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/fsm/CreateLagPortFsm.java @@ -26,7 +26,7 @@ import static org.openkilda.wfm.topology.switchmanager.fsm.CreateLagPortFsm.CreateLagState.GRPC_COMMAND_SEND; import static org.openkilda.wfm.topology.switchmanager.fsm.CreateLagPortFsm.CreateLagState.START; -import org.openkilda.messaging.command.grpc.CreateLogicalPortRequest; +import org.openkilda.messaging.command.grpc.CreateOrUpdateLogicalPortRequest; import org.openkilda.messaging.info.InfoMessage; import org.openkilda.messaging.model.grpc.LogicalPort; import org.openkilda.messaging.swmanager.request.CreateLagPortRequest; @@ -65,7 +65,7 @@ public class CreateLagPortFsm extends AbstractStateMachine< @Getter private final CreateLagPortRequest request; private final SwitchManagerCarrier carrier; - private CreateLogicalPortRequest grpcRequest; + private CreateOrUpdateLogicalPortRequest grpcRequest; private Integer lagLogicalPortNumber; public CreateLagPortFsm(SwitchManagerCarrier carrier, String key, CreateLagPortRequest request, @@ -121,7 +121,8 @@ void createLagInDb(CreateLagState from, CreateLagState to, CreateLagEvent event, lagLogicalPortNumber = lagPortOperationService.createLagPort(switchId, targetPorts); String ipAddress = lagPortOperationService.getSwitchIpAddress(switchId); - grpcRequest = new CreateLogicalPortRequest(ipAddress, request.getPortNumbers(), lagLogicalPortNumber, LAG); + grpcRequest = new CreateOrUpdateLogicalPortRequest( + ipAddress, request.getPortNumbers(), lagLogicalPortNumber, LAG); } catch (InvalidDataException | InconsistentDataException | SwitchNotFoundException e) { log.error(format("Unable to handle %s. Error: %s", request, e.getMessage()), e); fire(ERROR, CreateLagContext.builder().error(e).build()); diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/fsm/SwitchSyncFsm.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/fsm/SwitchSyncFsm.java index b9923a2fe56..fe568c2fa91 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/fsm/SwitchSyncFsm.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/fsm/SwitchSyncFsm.java @@ -51,7 +51,7 @@ import org.openkilda.messaging.command.flow.ReinstallDefaultFlowForSwitchManagerRequest; import org.openkilda.messaging.command.flow.RemoveFlow; import org.openkilda.messaging.command.flow.RemoveFlowForSwitchManagerRequest; -import org.openkilda.messaging.command.grpc.CreateLogicalPortRequest; +import org.openkilda.messaging.command.grpc.CreateOrUpdateLogicalPortRequest; import org.openkilda.messaging.command.grpc.DeleteLogicalPortRequest; import org.openkilda.messaging.command.switches.DeleteGroupRequest; import org.openkilda.messaging.command.switches.DeleterMeterForSwitchManagerRequest; @@ -124,7 +124,7 @@ public class SwitchSyncFsm extends AbstractBaseFsm missingGroups = emptyList(); private List misconfiguredGroups = emptyList(); private List excessGroups = emptyList(); - private List missingLogicalPorts = emptyList(); + private List missingLogicalPorts = emptyList(); private List excessLogicalPorts = emptyList(); private int missingRulesPendingResponsesCount = 0; @@ -520,7 +520,7 @@ protected void sendLogicalPortsCommandsCommands(SwitchSyncState from, SwitchSync log.info("Request to install logical ports has been sent (switch={}, key={})", switchId, key); missingLogicalPortsPendingResponsesCount = missingLogicalPorts.size(); - for (CreateLogicalPortRequest createRequest : missingLogicalPorts) { + for (CreateOrUpdateLogicalPortRequest createRequest : missingLogicalPorts) { carrier.sendCommandToSpeaker(key, createRequest); } } diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/CommandBuilder.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/CommandBuilder.java index 4be0a4d3fbe..acc280dc48d 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/CommandBuilder.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/CommandBuilder.java @@ -19,7 +19,7 @@ import org.openkilda.messaging.command.flow.ModifyDefaultMeterForSwitchManagerRequest; import org.openkilda.messaging.command.flow.ReinstallDefaultFlowForSwitchManagerRequest; import org.openkilda.messaging.command.flow.RemoveFlow; -import org.openkilda.messaging.command.grpc.CreateLogicalPortRequest; +import org.openkilda.messaging.command.grpc.CreateOrUpdateLogicalPortRequest; import org.openkilda.messaging.command.grpc.DeleteLogicalPortRequest; import org.openkilda.messaging.info.rule.FlowEntry; import org.openkilda.messaging.info.switches.LogicalPortInfoEntry; @@ -45,7 +45,7 @@ List buildCommandsToModifyMisconfigur List buildGroupInstallContexts(SwitchId switchId, List groupIds); - List buildLogicalPortInstallCommands( + List buildLogicalPortInstallCommands( SwitchId switchId, List missingLogicalPorts); List buildLogicalPortDeleteCommands(SwitchId switchId, List excessLogicalPorts); diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/CreateLagPortService.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/CreateLagPortService.java index b1632dc2e58..2a2ea9f042e 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/CreateLagPortService.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/CreateLagPortService.java @@ -18,7 +18,7 @@ import org.openkilda.messaging.MessageCookie; import org.openkilda.messaging.error.ErrorData; import org.openkilda.messaging.info.InfoData; -import org.openkilda.messaging.info.grpc.CreateLogicalPortResponse; +import org.openkilda.messaging.info.grpc.CreateOrUpdateLogicalPortResponse; import org.openkilda.messaging.swmanager.request.CreateLagPortRequest; import org.openkilda.wfm.error.MessageDispatchException; import org.openkilda.wfm.error.UnexpectedInputException; @@ -95,8 +95,8 @@ public void timeout(@NonNull MessageCookie cookie) throws MessageDispatchExcepti @Override public void dispatchWorkerMessage(InfoData payload, MessageCookie cookie) throws UnexpectedInputException, MessageDispatchException { - if (payload instanceof CreateLogicalPortResponse) { - handleCreateOrUpdateResponse((CreateLogicalPortResponse) payload, cookie); + if (payload instanceof CreateOrUpdateLogicalPortResponse) { + handleCreateOrUpdateResponse((CreateOrUpdateLogicalPortResponse) payload, cookie); } else { throw new UnexpectedInputException(payload); } @@ -111,7 +111,7 @@ public void dispatchWorkerMessage(ErrorData payload, MessageCookie cookie) fireFsmEvent(cookie, CreateLagEvent.ERROR, context); } - private void handleCreateOrUpdateResponse(CreateLogicalPortResponse payload, MessageCookie cookie) + private void handleCreateOrUpdateResponse(CreateOrUpdateLogicalPortResponse payload, MessageCookie cookie) throws MessageDispatchException { fireFsmEvent(cookie, CreateLagEvent.LAG_INSTALLED, CreateLagContext.builder().createdLogicalPort(payload.getLogicalPort()).build()); diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/LagPortOperationService.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/LagPortOperationService.java index 9ccd9606f4e..5defb05e5ee 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/LagPortOperationService.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/LagPortOperationService.java @@ -98,16 +98,26 @@ public LagPortOperationService(LagPortOperationConfig config) { /** * Create LAG logical port. */ - public int createLagPort(SwitchId switchId, Set physicalPortNumbers) { - if (physicalPortNumbers == null || physicalPortNumbers.isEmpty()) { - throw new InvalidDataException("Physical ports list is empty"); - } - + public int createLagPort(SwitchId switchId, Set targetPorts) { + verifyTargetPortsInput(targetPorts); LagLogicalPort port = transactionManager.doInTransaction( - newCreateRetryPolicy(switchId), () -> createTransaction(switchId, physicalPortNumbers)); + newCreateRetryPolicy(switchId), () -> createTransaction(switchId, targetPorts)); return port.getLogicalPortNumber(); } + /** + * Update LAG logical port. + */ + public Set updateLagPort(SwitchId switchId, int logicalPortNumber, Set targetPorts) { + verifyTargetPortsInput(targetPorts); + Set targetsBeforeUpdate = new HashSet<>(); + transactionManager.doInTransaction( + newUpdateRetryPolicy(switchId), + () -> targetsBeforeUpdate.addAll(updateTransaction(switchId, logicalPortNumber, targetPorts))); + return targetsBeforeUpdate; + } + + /** * Delete LAG logical port. */ @@ -121,11 +131,7 @@ public LagLogicalPort removeLagPort(SwitchId switchId, int logicalPortNumber) { */ public LagLogicalPort ensureDeleteIsPossible(SwitchId switchId, int logicalPortNumber) { Switch sw = querySwitch(switchId); // locate switch first to produce correct error if switch is missing - Optional port = lagLogicalPortRepository.findBySwitchIdAndPortNumber( - switchId, logicalPortNumber); - if (!port.isPresent()) { - throw new LagPortNotFoundException(switchId, logicalPortNumber); - } + LagLogicalPort port = queryLagPort(switchId, logicalPortNumber); List occupiedBy = flowRepository.findByEndpoint(switchId, logicalPortNumber).stream() .map(Flow::getFlowId) @@ -141,7 +147,7 @@ public LagLogicalPort ensureDeleteIsPossible(SwitchId switchId, int logicalPortN logicalPortNumber, switchId); } - return port.get(); + return port; } private void validatePhysicalPort(SwitchId switchId, Set features, Integer portNumber) @@ -213,30 +219,44 @@ public String getSwitchIpAddress(SwitchId switchId) throws InvalidDataException, format("Switch %s has invalid IP address %s", sw, sw.getSocketAddress()))); } - private LagLogicalPort createTransaction(SwitchId switchId, Set targetPorts) { - Switch sw = querySwitch(switchId); // locate switch first to produce correct error if switch is missing - if (! isSwitchLagCapable(sw)) { - throw new InvalidDataException(format("Switch %s doesn't support LAG.", sw.getSwitchId())); - } - - Set features = sw.getFeatures(); - for (Integer portNumber : targetPorts) { - validatePhysicalPort(switchId, features, portNumber); + private void verifyTargetPortsInput(Set targetPorts) { + if (targetPorts == null || targetPorts.isEmpty()) { + throw new InvalidDataException("Physical ports list is empty"); } + } - ensureNoLagCollisions(switchId, targetPorts); + private LagLogicalPort createTransaction(SwitchId switchId, Set targetPorts) { + Switch sw = querySwitch(switchId); // locate switch first to produce correct error if switch is missing + ensureLagDataValid(sw, targetPorts); + ensureNoLagCollisions(sw.getSwitchId(), targetPorts); LagLogicalPort port = queryPoolManager(switchId).allocate(); - port.setPhysicalPorts( - targetPorts.stream() - .map(portNumber -> new PhysicalPort(switchId, portNumber, port)) - .collect(Collectors.toList())); + replacePhysicalPorts(port, switchId, targetPorts); log.info("Adding new LAG logical port entry into DB: {}", port); lagLogicalPortRepository.add(port); return port; } + private Set updateTransaction(SwitchId switchId, int logicalPortNumber, Set targetPorts) { + Switch sw = querySwitch(switchId); + ensureLagDataValid(sw, targetPorts); + ensureNoLagCollisions(switchId, targetPorts, logicalPortNumber); + + LagLogicalPort port = queryLagPort(sw.getSwitchId(), logicalPortNumber); + Set targetsBeforeUpdate = port.getPhysicalPorts().stream() + .map(PhysicalPort::getPortNumber) + .collect(Collectors.toSet()); + log.info( + "Updating LAG logical port #{} on {} entry desired target ports set {}, current target ports set {}", + logicalPortNumber, switchId, + formatPortNumbersSet(targetPorts), formatPortNumbersSet(targetsBeforeUpdate)); + replacePhysicalPorts(port, switchId, targetPorts); + + + return targetsBeforeUpdate; + } + private LagLogicalPort deleteTransaction(SwitchId switchId, int logicalPortNumber) { LagLogicalPort port = ensureDeleteIsPossible(switchId, logicalPortNumber); log.info("Removing LAG logical port entry into DB: {}", port); @@ -244,14 +264,49 @@ private LagLogicalPort deleteTransaction(SwitchId switchId, int logicalPortNumbe return port; } + private void ensureLagDataValid(Switch sw, Set targetPorts) { + ensureSwitchIsLagCapable(sw); + ensureTargetPortsValid(sw, targetPorts); + } + + private void ensureSwitchIsLagCapable(Switch sw) { + if (!isSwitchLagCapable(sw)) { + throw new InvalidDataException(format("Switch %s doesn't support LAG.", sw.getSwitchId())); + } + } + private boolean isSwitchLagCapable(Switch sw) { return sw.getFeatures().contains(SwitchFeature.LAG); } + private void ensureTargetPortsValid(Switch sw, Set targetPorts) { + Set features = sw.getFeatures(); + SwitchId switchId = sw.getSwitchId(); + for (Integer portNumber : targetPorts) { + validatePhysicalPort(switchId, features, portNumber); + } + } + private void ensureNoLagCollisions(SwitchId switchId, Set targetPorts) { - Set occupiedPorts = physicalPortRepository.findPortNumbersBySwitchId(switchId); + ensureNoLagCollisions(switchId, targetPorts, null); + } + + private void ensureNoLagCollisions(SwitchId switchId, Set targetPorts, Integer excludeLogicalPort) { // FIXME(surabujin): we are unreasonably supposing that all physical port objects in DB related to LAGs - SetView intersection = Sets.intersection(occupiedPorts, new HashSet<>(targetPorts)); + Collection occupiedPorts = physicalPortRepository.findBySwitchId(switchId); + Set deniedTargets = new HashSet<>(); + for (PhysicalPort entry : occupiedPorts) { + LagLogicalPort partOf = entry.getLagLogicalPort(); + if (excludeLogicalPort != null && partOf != null && excludeLogicalPort == partOf.getLogicalPortNumber()) { + log.debug( + "Exclude port #{} on {} from collision list, because it is part of {} LAG logical port", + entry.getPortNumber(), switchId, excludeLogicalPort); + continue; + } + deniedTargets.add(entry.getPortNumber()); + } + + SetView intersection = Sets.intersection(deniedTargets, new HashSet<>(targetPorts)); if (! intersection.isEmpty()) { throw new InvalidDataException( @@ -270,6 +325,18 @@ private Switch querySwitch(SwitchId switchId) throws SwitchNotFoundException { return switchRepository.findById(switchId).orElseThrow(() -> new SwitchNotFoundException(switchId)); } + private LagLogicalPort queryLagPort(SwitchId switchId, int logicalPortNumber) { + Optional port = lagLogicalPortRepository.findBySwitchIdAndPortNumber( + switchId, logicalPortNumber); + return port.orElseThrow(() -> new LagPortNotFoundException(switchId, logicalPortNumber)); + } + + private void replacePhysicalPorts(LagLogicalPort port, SwitchId switchId, Set targetPorts) { + port.setPhysicalPorts(targetPorts.stream() + .map(portNumber -> new PhysicalPort(switchId, portNumber, port)) + .collect(Collectors.toList())); + } + private PoolManager newPoolManager(SwitchId switchId) { LagPortPoolEntityAdapter adapter = new LagPortPoolEntityAdapter(config, lagLogicalPortRepository, switchId); return new PoolManager<>(config.getPoolConfig(), adapter); @@ -279,6 +346,10 @@ private RetryPolicy newCreateRetryPolicy(SwitchId switchId) { return newRetryPolicy(switchId, "create"); } + private RetryPolicy newUpdateRetryPolicy(SwitchId switchId) { + return newRetryPolicy(switchId, "update"); + } + private RetryPolicy newDeleteRetryPolicy(SwitchId switchId) { return newRetryPolicy(switchId, "delete"); } @@ -295,4 +366,11 @@ private RetryPolicy newRetryPolicy(SwitchId switchId, String act "Failed to {} LAG logical port DB record for switch {}: {}", action, switchId, e.getFailure().getMessage(), e.getFailure())); } + + private static String formatPortNumbersSet(Set ports) { + return ports.stream() + .sorted() + .map(Objects::toString) + .collect(Collectors.joining(", ", "{", "}")); + } } diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchSyncService.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchSyncService.java index 2742ab2922b..dbd7f0e0fd8 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchSyncService.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/SwitchSyncService.java @@ -22,7 +22,7 @@ import org.openkilda.messaging.info.flow.FlowInstallResponse; import org.openkilda.messaging.info.flow.FlowReinstallResponse; import org.openkilda.messaging.info.flow.FlowRemoveResponse; -import org.openkilda.messaging.info.grpc.CreateLogicalPortResponse; +import org.openkilda.messaging.info.grpc.CreateOrUpdateLogicalPortResponse; import org.openkilda.messaging.info.grpc.DeleteLogicalPortResponse; import org.openkilda.messaging.info.switches.DeleteGroupResponse; import org.openkilda.messaging.info.switches.DeleteMeterResponse; @@ -101,7 +101,7 @@ public void dispatchWorkerMessage(InfoData payload, MessageCookie cookie) fireHandlerEvent(cookie, SwitchSyncEvent.GROUPS_MODIFIED); } else if (payload instanceof DeleteGroupResponse) { fireHandlerEvent(cookie, SwitchSyncEvent.GROUPS_REMOVED); - } else if (payload instanceof CreateLogicalPortResponse) { + } else if (payload instanceof CreateOrUpdateLogicalPortResponse) { fireHandlerEvent(cookie, SwitchSyncEvent.LOGICAL_PORT_INSTALLED); } else if (payload instanceof DeleteLogicalPortResponse) { fireHandlerEvent(cookie, SwitchSyncEvent.LOGICAL_PORT_REMOVED); diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/UpdateLagPortService.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/UpdateLagPortService.java new file mode 100644 index 00000000000..a80c7195631 --- /dev/null +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/UpdateLagPortService.java @@ -0,0 +1,188 @@ +/* Copyright 2022 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.switchmanager.service; + +import org.openkilda.messaging.MessageCookie; +import org.openkilda.messaging.error.ErrorData; +import org.openkilda.messaging.error.ErrorType; +import org.openkilda.messaging.info.InfoData; +import org.openkilda.messaging.info.grpc.CreateOrUpdateLogicalPortResponse; +import org.openkilda.messaging.swmanager.request.UpdateLagPortRequest; +import org.openkilda.wfm.error.MessageDispatchException; +import org.openkilda.wfm.error.UnexpectedInputException; +import org.openkilda.wfm.topology.switchmanager.error.InconsistentDataException; +import org.openkilda.wfm.topology.switchmanager.error.SwitchManagerException; +import org.openkilda.wfm.topology.switchmanager.service.handler.LagPortUpdateHandler; + +import com.google.common.annotations.VisibleForTesting; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; + +@Slf4j +public class UpdateLagPortService implements SwitchManagerHubService { + @Getter + private final SwitchManagerCarrier carrier; + + private final LagPortOperationService operationService; + + @VisibleForTesting + final Map activeHandlers = new HashMap<>(); + + private boolean active = true; + + public UpdateLagPortService(SwitchManagerCarrier carrier, LagPortOperationConfig config) { + this(carrier, new LagPortOperationService(config)); + } + + public UpdateLagPortService(SwitchManagerCarrier carrier, LagPortOperationService operationService) { + this.carrier = carrier; + this.operationService = operationService; + } + + /** + * Handle update request. + */ + public void update(String requestKey, UpdateLagPortRequest request) { + LagPortUpdateHandler handler = newHandler(requestKey, request); + LagPortUpdateHandler existing = activeHandlers.put( + requestKey, handler); + if (existing != null) { + activeHandlers.put(requestKey, existing); // restore handler + throw new InconsistentDataException( + String.format( + "LAG logical port update requests collision, requests with key=%s already exists", + requestKey)); + } + + process(requestKey, handler, (h, dummy) -> h.start()); + } + + @Override + public void timeout(@NonNull MessageCookie cookie) { + try { + process(cookie, (h, dummy) -> { + log.debug("Got timeout notification for {}", h.formatLagPortReference()); + h.timeout(); + }); + } catch (MessageDispatchException e) { + log.debug("There is no handler for timeout notification {}", cookie); + } + } + + @Override + public void dispatchWorkerMessage(InfoData payload, MessageCookie cookie) + throws MessageDispatchException, UnexpectedInputException { + if (payload instanceof CreateOrUpdateLogicalPortResponse) { + process(cookie, (h, nested) -> h.dispatchGrpcResponse((CreateOrUpdateLogicalPortResponse) payload, nested)); + } else { + throw new UnexpectedInputException(payload); + } + } + + @Override + public void dispatchWorkerMessage(ErrorData payload, MessageCookie cookie) throws MessageDispatchException { + process(cookie, (h, nested) -> h.dispatchGrpcResponse(payload, nested)); + } + + @Override + public void activate() { + active = true; + } + + @Override + public boolean deactivate() { + active = false; + return isAllOperationsCompleted(); + } + + @Override + public boolean isAllOperationsCompleted() { + return ! active && activeHandlers.isEmpty(); + } + + private void process(MessageCookie cookie, BiConsumer action) + throws MessageDispatchException { + if (cookie == null) { + throw new MessageDispatchException(); + } + + LagPortUpdateHandler handler = activeHandlers.get(cookie.getValue()); + if (handler == null) { + throw new MessageDispatchException(cookie); + } + process(cookie.getValue(), handler, cookie.getNested(), action); + } + + private void process( + String requestKey, LagPortUpdateHandler handler, BiConsumer action) { + process(requestKey, handler, null, action); + } + + private void process( + String requestKey, LagPortUpdateHandler handler, MessageCookie cookie, + BiConsumer action) { + boolean success = false; + try { + action.accept(handler, cookie); + success = true; + } catch (SwitchManagerException e) { + errorResponse(requestKey, e.getError(), handler.getGoal(), e.getMessage()); + } catch (Exception e) { + UpdateLagPortRequest goal = handler.getGoal(); + log.error( + "Error processing LAG update request for port #{} on {} with request key {}: {}", + goal.getLogicalPortNumber(), goal.getSwitchId(), requestKey, e.getMessage(), + e); + errorResponse( + requestKey, ErrorType.INTERNAL_ERROR, goal, "Internal error, see application logs for details"); + } finally { + if (! success || handler.isCompleted()) { + UpdateLagPortRequest goal = handler.getGoal(); + log.debug( + "Remove LAG logical port #{} on {} update handler (isSuccess: {}, isComplete: {})", + goal.getLogicalPortNumber(), goal.getSwitchId(), + success, handler.isCompleted()); + activeHandlers.remove(requestKey); + carrier.cancelTimeoutCallback(requestKey); + processPossiblePostponedDeactivation(); + } + } + } + + private void errorResponse(String requestKey, ErrorType type, UpdateLagPortRequest goal, String description) { + String message = String.format( + "Error processing LAG logical port #%d on %s update request", + goal.getLogicalPortNumber(), goal.getSwitchId()); + carrier.errorResponse(requestKey, type, message, description); + } + + private void processPossiblePostponedDeactivation() { + if (! active && activeHandlers.isEmpty()) { + carrier.sendInactive(); + } + } + + private LagPortUpdateHandler newHandler(String requestKey, UpdateLagPortRequest request) { + return new LagPortUpdateHandler( + new SwitchManagerCarrierCookieDecorator(carrier, requestKey), operationService, + requestKey, request); + } +} diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/handler/LagPortUpdateHandler.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/handler/LagPortUpdateHandler.java new file mode 100644 index 00000000000..17bea073936 --- /dev/null +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/handler/LagPortUpdateHandler.java @@ -0,0 +1,187 @@ +/* Copyright 2022 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.switchmanager.service.handler; + +import org.openkilda.messaging.MessageCookie; +import org.openkilda.messaging.MessageData; +import org.openkilda.messaging.command.grpc.CreateOrUpdateLogicalPortRequest; +import org.openkilda.messaging.error.ErrorData; +import org.openkilda.messaging.error.ErrorType; +import org.openkilda.messaging.info.grpc.CreateOrUpdateLogicalPortResponse; +import org.openkilda.messaging.model.grpc.LogicalPortType; +import org.openkilda.messaging.swmanager.request.UpdateLagPortRequest; +import org.openkilda.messaging.swmanager.response.LagPortResponse; +import org.openkilda.wfm.topology.switchmanager.error.SwitchManagerException; +import org.openkilda.wfm.topology.switchmanager.service.LagPortOperationService; +import org.openkilda.wfm.topology.switchmanager.service.SwitchManagerCarrier; + +import com.fasterxml.uuid.Generators; +import com.fasterxml.uuid.NoArgGenerator; +import com.google.common.annotations.VisibleForTesting; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +public class LagPortUpdateHandler { + private static final int COOKIE_GENERATION_ATTEMPTS_LIMIT = 5; + private static final NoArgGenerator cookieValueGenerator = Generators.randomBasedGenerator(); + + private final SwitchManagerCarrier carrier; + + private final LagPortOperationService operationService; + + private final String requestKey; + @Getter + private final UpdateLagPortRequest goal; + + @VisibleForTesting + Set rollbackTargets; + + private final Set pendingSpeakerRequests = new HashSet<>(); + + public LagPortUpdateHandler( + SwitchManagerCarrier carrier, LagPortOperationService operationService, String requestKey, + UpdateLagPortRequest goal) { + this.carrier = carrier; + this.operationService = operationService; + this.requestKey = requestKey; + this.goal = goal; + } + + /** + * Handle start event. + */ + public void start() { + Set targetPorts = new HashSet<>(goal.getTargetPorts()); + rollbackTargets = operationService.updateLagPort(goal.getSwitchId(), goal.getLogicalPortNumber(), targetPorts); + + CreateOrUpdateLogicalPortRequest request = newGrpcRequest(targetPorts); + MessageCookie cookie = newMessageCookie(); + + log.info( + "Going to update {}, target ports set: {}", + formatLagPortReference(), formatTargetPorts(goal.getTargetPorts())); + carrier.sendCommandToSpeaker(request, cookie); + pendingSpeakerRequests.add(cookie); + } + + /** + * Handle GRPC response. + */ + public void dispatchGrpcResponse(CreateOrUpdateLogicalPortResponse response, MessageCookie cookie) { + if (! pendingSpeakerRequests.remove(cookie)) { + logUnwantedResponse(response); + return; + } + + log.info("{} have been updated", formatLagPortReference()); + carrier.response(requestKey, new LagPortResponse(goal.getLogicalPortNumber(), goal.getTargetPorts())); + } + + /** + * Handle GRPC error response. + */ + public void dispatchGrpcResponse(ErrorData response, MessageCookie cookie) { + if (!pendingSpeakerRequests.remove(cookie)) { + logUnwantedResponse(response); + return; + } + + log.error("Unable to update {}: {}", formatLagPortReference(), response); + + // TODO(surabujin): should we retry update attempt? + + fail(response.getErrorType(), response.getErrorMessage()); + } + + /** + * Handle timeout event. + */ + public void timeout() { + fail(ErrorType.OPERATION_TIMED_OUT, "Timeout communication switch via GRPC"); + + pendingSpeakerRequests.clear(); // force handle completion + } + + public boolean isCompleted() { + return pendingSpeakerRequests.isEmpty(); + } + + private void fail(ErrorType type, String description) { + String errorMessage; + if (rollback()) { + errorMessage = String.format("Unable to update %s", formatLagPortReference()); + } else { + errorMessage = String.format( + "Unable to update %s, also DB data rollback have failed, use switch validate/sync to restore " + + "system's consistent state", + formatLagPortReference()); + } + + carrier.errorResponse(requestKey, type, errorMessage, description); + } + + private boolean rollback() { + try { + operationService.updateLagPort(goal.getSwitchId(), goal.getLogicalPortNumber(), rollbackTargets); + } catch (SwitchManagerException e) { + log.error("Unable to rollback DB update for {}: {}", formatLagPortReference(), e.getMessage()); + return false; + } + return true; + } + + private void logUnwantedResponse(MessageData payload) { + log.info("Got unwanted/outdated GRPC response: {}", payload); + } + + private MessageCookie newMessageCookie() { + for (int i = 0; i < COOKIE_GENERATION_ATTEMPTS_LIMIT; i++) { + MessageCookie attempt = new MessageCookie(cookieValueGenerator.generate().toString()); + if (pendingSpeakerRequests.contains(attempt)) { + continue; + } + + return attempt; + } + + throw new IllegalStateException(String.format( + "Unable to generate request cookie (made %d attempts)", COOKIE_GENERATION_ATTEMPTS_LIMIT)); + } + + private CreateOrUpdateLogicalPortRequest newGrpcRequest(Set targetPorts) { + String address = operationService.getSwitchIpAddress(goal.getSwitchId()); + return new CreateOrUpdateLogicalPortRequest( + address, new ArrayList<>(targetPorts), goal.getLogicalPortNumber(), LogicalPortType.LAG); + } + + public String formatLagPortReference() { + return String.format("LAG logical port #%d on %s", goal.getLogicalPortNumber(), goal.getSwitchId()); + } + + private static String formatTargetPorts(List origin) { + List ports = new ArrayList<>(origin); + Collections.sort(ports); + return ports.stream().map(Object::toString).collect(Collectors.joining(", ")); + } +} diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/CommandBuilderImpl.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/CommandBuilderImpl.java index 8a4e1b2c1ca..ae6770c24be 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/CommandBuilderImpl.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/service/impl/CommandBuilderImpl.java @@ -30,7 +30,7 @@ import org.openkilda.messaging.command.flow.ReinstallDefaultFlowForSwitchManagerRequest; import org.openkilda.messaging.command.flow.ReinstallServer42FlowForSwitchManagerRequest; import org.openkilda.messaging.command.flow.RemoveFlow; -import org.openkilda.messaging.command.grpc.CreateLogicalPortRequest; +import org.openkilda.messaging.command.grpc.CreateOrUpdateLogicalPortRequest; import org.openkilda.messaging.command.grpc.DeleteLogicalPortRequest; import org.openkilda.messaging.command.switches.DeleteRulesCriteria; import org.openkilda.messaging.info.rule.FlowApplyActions; @@ -467,13 +467,13 @@ public List buildGroupInstallContexts(SwitchId switchId, Li } @Override - public List buildLogicalPortInstallCommands( + public List buildLogicalPortInstallCommands( SwitchId switchId, List missingLogicalPorts) { String ipAddress = getSwitchIpAddress(switchId); - List requests = new ArrayList<>(); + List requests = new ArrayList<>(); for (LogicalPortInfoEntry port : missingLogicalPorts) { - requests.add(new CreateLogicalPortRequest( + requests.add(new CreateOrUpdateLogicalPortRequest( ipAddress, port.getPhysicalPorts(), port.getLogicalPortNumber(), LogicalPortMapper.INSTANCE.map(port.getType()))); } diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/UpdateLagPortServiceTest.java b/src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/UpdateLagPortServiceTest.java new file mode 100644 index 00000000000..eeb939c269d --- /dev/null +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/UpdateLagPortServiceTest.java @@ -0,0 +1,91 @@ +/* Copyright 2022 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.switchmanager.service; + +import org.openkilda.messaging.error.ErrorType; +import org.openkilda.messaging.swmanager.request.UpdateLagPortRequest; +import org.openkilda.model.SwitchId; +import org.openkilda.persistence.repositories.RepositoryFactory; +import org.openkilda.persistence.tx.TransactionManager; +import org.openkilda.wfm.topology.switchmanager.error.InconsistentDataException; +import org.openkilda.wfm.topology.switchmanager.error.SwitchNotFoundException; +import org.openkilda.wfm.topology.switchmanager.service.handler.LagPortUpdateHandler; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; + +@RunWith(MockitoJUnitRunner.class) +public class UpdateLagPortServiceTest { + @Mock + private SwitchManagerCarrier carrier; + + @Mock + private LagPortOperationService operationService; + + @Mock + RepositoryFactory repositoryFactory; + + @Mock + TransactionManager transactionManager; + + @Test + public void testKeepHandlerOnRequestKeyCollision() { + LagPortOperationConfig config = newConfig(); + UpdateLagPortService subject = new UpdateLagPortService(carrier, operationService); + + String requestKey = "test-key"; + Assert.assertFalse(subject.activeHandlers.containsKey(requestKey)); + + UpdateLagPortRequest request = new UpdateLagPortRequest( + new SwitchId(1), (int) config.getPoolConfig().getIdMinimum(), Arrays.asList(1, 2, 3)); + subject.update(requestKey, request); + LagPortUpdateHandler origin = subject.activeHandlers.get(requestKey); + Assert.assertNotNull(origin); + + UpdateLagPortRequest request2 = new UpdateLagPortRequest( + new SwitchId(2), (int) config.getPoolConfig().getIdMinimum(), Arrays.asList(1, 2, 3)); + Assert.assertThrows(InconsistentDataException.class, () -> subject.update(requestKey, request2)); + Assert.assertSame(origin, subject.activeHandlers.get(requestKey)); + } + + @Test + public void testHandlerRemoveOnException() { + LagPortOperationConfig config = newConfig(); + UpdateLagPortService subject = new UpdateLagPortService(carrier, operationService); + + SwitchId switchId = new SwitchId(1); + Mockito.when(operationService.getSwitchIpAddress(switchId)).thenThrow(new SwitchNotFoundException(switchId)); + + String requestKey = "test-key"; + UpdateLagPortRequest request = new UpdateLagPortRequest( + switchId, (int) config.getPoolConfig().getIdMinimum(), Arrays.asList(1, 2, 3)); + subject.update(requestKey, request); + Mockito.verify(carrier).errorResponse( + Mockito.eq(requestKey), Mockito.eq(ErrorType.NOT_FOUND), Mockito.anyString(), Mockito.anyString()); + Assert.assertFalse(subject.activeHandlers.containsKey(requestKey)); + } + + private LagPortOperationConfig newConfig() { + return new LagPortOperationConfig( + repositoryFactory, transactionManager, 1000, 1999, 2000, 2999, 10, 100); + } +} diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/handler/LagPortUpdateHandlerTest.java b/src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/handler/LagPortUpdateHandlerTest.java new file mode 100644 index 00000000000..736930423ba --- /dev/null +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/test/java/org/openkilda/wfm/topology/switchmanager/service/handler/LagPortUpdateHandlerTest.java @@ -0,0 +1,178 @@ +/* Copyright 2022 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.switchmanager.service.handler; + +import org.openkilda.messaging.MessageCookie; +import org.openkilda.messaging.command.grpc.CreateOrUpdateLogicalPortRequest; +import org.openkilda.messaging.error.ErrorData; +import org.openkilda.messaging.error.ErrorType; +import org.openkilda.messaging.info.grpc.CreateOrUpdateLogicalPortResponse; +import org.openkilda.messaging.model.grpc.LogicalPort; +import org.openkilda.messaging.model.grpc.LogicalPortType; +import org.openkilda.messaging.swmanager.request.UpdateLagPortRequest; +import org.openkilda.messaging.swmanager.response.LagPortResponse; +import org.openkilda.model.SwitchId; +import org.openkilda.wfm.topology.switchmanager.error.SwitchManagerException; +import org.openkilda.wfm.topology.switchmanager.error.SwitchNotFoundException; +import org.openkilda.wfm.topology.switchmanager.service.LagPortOperationService; +import org.openkilda.wfm.topology.switchmanager.service.SwitchManagerCarrier; + +import com.google.common.collect.ImmutableSet; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +@RunWith(MockitoJUnitRunner.class) +public class LagPortUpdateHandlerTest { + @Mock + private SwitchManagerCarrier carrier; + + @Mock + private LagPortOperationService operationService; + + @Test + public void testHappyPath() { + String requestKey = "testRequest"; + String swAddress = "127.0.3.1"; + + UpdateLagPortRequest request = newRequest(); + LagPortUpdateHandler subject = new LagPortUpdateHandler(carrier, operationService, requestKey, request); + MessageCookie grpcRequestCookie = verifyStartHandler(subject, request, swAddress); + + // GRPC success response + subject.dispatchGrpcResponse( + new CreateOrUpdateLogicalPortResponse( + swAddress, + LogicalPort.builder() + .logicalPortNumber(request.getLogicalPortNumber()) + .portNumbers(request.getTargetPorts()) + .name("dummy") + .type(LogicalPortType.LAG) + .build(), + true), + grpcRequestCookie); + + Mockito.verifyNoMoreInteractions(operationService); + Mockito.verify(carrier).response( + Mockito.eq(requestKey), + Mockito.eq(new LagPortResponse(request.getLogicalPortNumber(), request.getTargetPorts()))); + Mockito.verifyNoMoreInteractions(carrier); + + Assert.assertTrue(subject.isCompleted()); + } + + @Test + public void testGrpcErrorResponse() { + String requestKey = "testRequest"; + String swAddress = "127.0.3.1"; + + UpdateLagPortRequest request = newRequest(); + LagPortUpdateHandler subject = new LagPortUpdateHandler(carrier, operationService, requestKey, request); + MessageCookie grpcRequestCookie = verifyStartHandler(subject, request, swAddress); + + // GRPC error response + ErrorData error = new ErrorData(ErrorType.INTERNAL_ERROR, "Dummy error message", "Dummy error description"); + subject.dispatchGrpcResponse(error, grpcRequestCookie); + Mockito.verify(operationService).updateLagPort( + Mockito.eq(request.getSwitchId()), Mockito.eq(request.getLogicalPortNumber()), + Mockito.eq(subject.rollbackTargets)); + Mockito.verifyNoMoreInteractions(operationService); + Mockito.verify(carrier).errorResponse( + Mockito.eq(requestKey), Mockito.eq(error.getErrorType()), Mockito.anyString(), Mockito.anyString()); + Mockito.verifyNoMoreInteractions(carrier); + + Assert.assertTrue(subject.isCompleted()); + } + + @Test + public void testTimeout() { + String requestKey = "testRequest"; + String swAddress = "127.0.3.1"; + + UpdateLagPortRequest request = newRequest(); + LagPortUpdateHandler subject = new LagPortUpdateHandler(carrier, operationService, requestKey, request); + verifyStartHandler(subject, request, swAddress); + + // GRPC error response + subject.timeout(); + Mockito.verify(operationService).updateLagPort( + Mockito.eq(request.getSwitchId()), Mockito.eq(request.getLogicalPortNumber()), + Mockito.eq(subject.rollbackTargets)); + Mockito.verifyNoMoreInteractions(operationService); + Mockito.verify(carrier).errorResponse( + Mockito.eq(requestKey), Mockito.eq(ErrorType.OPERATION_TIMED_OUT), + Mockito.anyString(), Mockito.anyString()); + Mockito.verifyNoMoreInteractions(carrier); + + Assert.assertTrue(subject.isCompleted()); + } + + @Test + public void testExceptionOnErrorFromOperationService() { + String requestKey = "testRequest"; + + UpdateLagPortRequest request = newRequest(); + LagPortUpdateHandler subject = new LagPortUpdateHandler(carrier, operationService, requestKey, request); + + Mockito.when(operationService.getSwitchIpAddress(request.getSwitchId())).thenThrow( + new SwitchNotFoundException(request.getSwitchId())); + Assert.assertThrows(SwitchManagerException.class, subject::start); + } + + private MessageCookie verifyStartHandler( + LagPortUpdateHandler subject, UpdateLagPortRequest request, String swAddress) { + Set existingTargets = ImmutableSet.of(3, 4, 5); + Mockito.when(operationService.getSwitchIpAddress(request.getSwitchId())).thenReturn(swAddress); + Mockito.when(operationService.updateLagPort( + Mockito.eq(request.getSwitchId()), Mockito.eq(request.getLogicalPortNumber()), Mockito.any())) + .thenReturn(existingTargets); + + Mockito.verifyNoInteractions(carrier); + Mockito.verifyNoInteractions(operationService); + + // DB update and GRPC request + subject.start(); + Assert.assertFalse(subject.isCompleted()); + Assert.assertEquals(existingTargets, subject.rollbackTargets); + + Mockito.verify(operationService).getSwitchIpAddress(Mockito.eq(request.getSwitchId())); + Mockito.verify(operationService).updateLagPort( + Mockito.eq(request.getSwitchId()), Mockito.eq(request.getLogicalPortNumber()), + Mockito.eq(new HashSet<>(request.getTargetPorts()))); + Mockito.verifyNoMoreInteractions(operationService); + + ArgumentCaptor grpcRequestCookie = ArgumentCaptor.forClass(MessageCookie.class); + Mockito.verify(carrier).sendCommandToSpeaker( + Mockito.eq(new CreateOrUpdateLogicalPortRequest( + swAddress, request.getTargetPorts(), request.getLogicalPortNumber(), LogicalPortType.LAG)), + grpcRequestCookie.capture()); + Mockito.verifyNoMoreInteractions(carrier); + + return grpcRequestCookie.getValue(); + } + + private UpdateLagPortRequest newRequest() { + return new UpdateLagPortRequest(new SwitchId(1), 2001, Arrays.asList(1, 2, 3)); + } +} diff --git a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/yflows/YFlowCreateSpec.groovy b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/yflows/YFlowCreateSpec.groovy index 2f43eb1d21b..d7adbd70512 100644 --- a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/yflows/YFlowCreateSpec.groovy +++ b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/yflows/YFlowCreateSpec.groovy @@ -21,7 +21,7 @@ import org.openkilda.messaging.error.MessageError import org.openkilda.messaging.info.event.PathNode import org.openkilda.messaging.payload.flow.FlowState import org.openkilda.model.SwitchFeature -import org.openkilda.northbound.dto.v2.switches.CreateLagPortDto +import org.openkilda.northbound.dto.v2.switches.LagPortRequest import org.openkilda.northbound.dto.v2.yflows.YFlowCreatePayload import org.openkilda.northbound.dto.v2.yflows.YFlowPingPayload import org.openkilda.testing.model.topology.TopologyDefinition.Switch @@ -400,7 +400,7 @@ source: switchId="${flow.sharedEndpoint.switchId}" port=${flow.sharedEndpoint.po def swT = topologyHelper.switchTriplets.find { it.shared.features.contains(SwitchFeature.LAG) } assumeTrue(swT != null, "Unable to find a switch that supports LAG") def portsArray = topology.getAllowedPortsForSwitch(swT.shared)[-2, -1] - def payload = new CreateLagPortDto(portNumbers: portsArray) + def payload = new LagPortRequest(portNumbers: portsArray) def lagPort = northboundV2.createLagLogicalPort(swT.shared.dpId, payload).logicalPortNumber when: "Try creating a y-flow with shared endpoint port being inside LAG" diff --git a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/server42/Server42FlowRttSpec.groovy b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/server42/Server42FlowRttSpec.groovy index 2cf36b07d10..f1755c3cd2d 100644 --- a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/server42/Server42FlowRttSpec.groovy +++ b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/server42/Server42FlowRttSpec.groovy @@ -31,7 +31,7 @@ import org.openkilda.northbound.dto.v2.flows.FlowPatchEndpoint import org.openkilda.northbound.dto.v2.flows.FlowPatchV2 import org.openkilda.northbound.dto.v2.flows.FlowRequestV2 import org.openkilda.northbound.dto.v2.flows.SwapFlowPayload -import org.openkilda.northbound.dto.v2.switches.CreateLagPortDto +import org.openkilda.northbound.dto.v2.switches.LagPortRequest import org.openkilda.testing.model.topology.TopologyDefinition.Switch import groovy.time.TimeCategory @@ -906,7 +906,7 @@ class Server42FlowRttSpec extends HealthCheckSpecification { when: "Create a LAG port on the src switch" def portsForLag = topology.getAllowedPortsForSwitch(switchPair.src)[-2, -1] - def payload = new CreateLagPortDto(portNumbers: portsForLag) + def payload = new LagPortRequest(portNumbers: portsForLag) def lagPort = northboundV2.createLagLogicalPort(switchPair.src.dpId, payload).logicalPortNumber and: "Create a flow" diff --git a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/switches/LagPortSpec.groovy b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/switches/LagPortSpec.groovy index 2b4d295e1d3..f57535ddcac 100644 --- a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/switches/LagPortSpec.groovy +++ b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/switches/LagPortSpec.groovy @@ -17,7 +17,7 @@ import org.openkilda.model.SwitchId import org.openkilda.northbound.dto.v1.flows.PingInput import org.openkilda.northbound.dto.v2.flows.FlowEndpointV2 import org.openkilda.northbound.dto.v2.flows.FlowMirrorPointPayload -import org.openkilda.northbound.dto.v2.switches.CreateLagPortDto +import org.openkilda.northbound.dto.v2.switches.LagPortRequest import org.openkilda.testing.model.topology.TopologyDefinition.Switch import org.openkilda.testing.service.grpc.GrpcService import org.openkilda.testing.service.traffexam.TraffExamService @@ -53,7 +53,7 @@ class LagPortSpec extends HealthCheckSpecification { def portsArray = topology.getAllowedPortsForSwitch(sw)[-2, -1] when: "Create a LAG" - def payload = new CreateLagPortDto(portNumbers: portsArray) + def payload = new LagPortRequest(portNumbers: portsArray) def createResponse = northboundV2.createLagLogicalPort(sw.dpId, payload) then: "Response reports successful creation of the LAG port" @@ -119,7 +119,7 @@ class LagPortSpec extends HealthCheckSpecification { } def traffgenSrcSwPort = switchPair.src.traffGens.switchPort[0] def portsArray = (topology.getAllowedPortsForSwitch(switchPair.src)[-2, -1] << traffgenSrcSwPort).unique() - def payload = new CreateLagPortDto(portNumbers: portsArray) + def payload = new LagPortRequest(portNumbers: portsArray) def lagPort = northboundV2.createLagLogicalPort(switchPair.src.dpId, payload).logicalPortNumber when: "Create a flow" @@ -162,7 +162,7 @@ class LagPortSpec extends HealthCheckSpecification { assumeTrue(swPair.asBoolean(), "Unable to find required switch in topology") def traffgenSrcSwPort = swPair.src.traffGens[0].switchPort def traffgenDstSwPort = swPair.src.traffGens[1].switchPort - def payload = new CreateLagPortDto(portNumbers: [traffgenSrcSwPort]) + def payload = new LagPortRequest(portNumbers: [traffgenSrcSwPort]) def lagPort = northboundV2.createLagLogicalPort(swPair.src.dpId, payload).logicalPortNumber when: "Create a flow" @@ -197,7 +197,7 @@ class LagPortSpec extends HealthCheckSpecification { given: "A switch with a LAG port" def sw = topology.getActiveSwitches().first() def portsArray = topology.getAllowedPortsForSwitch(sw)[-2, -1] - def payload = new CreateLagPortDto(portNumbers: portsArray) + def payload = new LagPortRequest(portNumbers: portsArray) def lagPort = northboundV2.createLagLogicalPort(sw.dpId, payload).logicalPortNumber when: "Disconnect the switch" @@ -229,7 +229,7 @@ class LagPortSpec extends HealthCheckSpecification { given: "A flow on a LAG port" def switchPair = topologyHelper.getSwitchPairs().first() def portsArray = topology.getAllowedPortsForSwitch(switchPair.src)[-2, -1] - def payload = new CreateLagPortDto(portNumbers: portsArray) + def payload = new LagPortRequest(portNumbers: portsArray) def lagPort = northboundV2.createLagLogicalPort(switchPair.src.dpId, payload).logicalPortNumber def flow = flowHelperV2.randomFlow(switchPair).tap { source.portNumber = lagPort } flowHelperV2.addFlow(flow) @@ -258,7 +258,7 @@ class LagPortSpec extends HealthCheckSpecification { flowHelperV2.addFlow(flow) when: "Create a LAG port with flow's port" - northboundV2.createLagLogicalPort(sw.dpId, new CreateLagPortDto(portNumbers: [flow.source.portNumber])) + northboundV2.createLagLogicalPort(sw.dpId, new LagPortRequest(portNumbers: [flow.source.portNumber])) then: "Human readable error is returned" def exc = thrown(HttpClientErrorException) @@ -278,7 +278,7 @@ class LagPortSpec extends HealthCheckSpecification { given: "An active switch with LAG port on it" def sw = topology.activeSwitches.first() def portsArray = topology.getAllowedPortsForSwitch(sw)[-2, -1] - def payload = new CreateLagPortDto(portNumbers: portsArray) + def payload = new LagPortRequest(portNumbers: portsArray) def lagPort = northboundV2.createLagLogicalPort(sw.dpId, payload).logicalPortNumber when: "Create flow on ports which are in inside LAG group" @@ -321,7 +321,7 @@ class LagPortSpec extends HealthCheckSpecification { flowHelperV2.createMirrorPoint(flow.flowId, mirrorEndpoint) when: "Create a LAG port with port which is used as mirrorPort" - northboundV2.createLagLogicalPort(swP.src.dpId, new CreateLagPortDto(portNumbers: [mirrorPort])) + northboundV2.createLagLogicalPort(swP.src.dpId, new LagPortRequest(portNumbers: [mirrorPort])) then: "Human readable error is returned" def exc = thrown(HttpClientErrorException) @@ -341,7 +341,7 @@ class LagPortSpec extends HealthCheckSpecification { when: "Create a LAG port on a occupied port" def sw = topology.getActiveServer42Switches().first() def occupiedPort = data.portNumber(sw) - northboundV2.createLagLogicalPort(sw.dpId, new CreateLagPortDto(portNumbers: [occupiedPort])) + northboundV2.createLagLogicalPort(sw.dpId, new LagPortRequest(portNumbers: [occupiedPort])) then: "Human readable error is returned" def exc = thrown(HttpClientErrorException) @@ -380,11 +380,11 @@ class LagPortSpec extends HealthCheckSpecification { def availablePorts = topology.getAllowedPortsForSwitch(sw) def portsArray = availablePorts[-2, -1] def conflictPortsArray = availablePorts[-3, -1] - def payload = new CreateLagPortDto(portNumbers: portsArray) + def payload = new LagPortRequest(portNumbers: portsArray) def lagPort = northboundV2.createLagLogicalPort(sw.dpId, payload).logicalPortNumber when: "Try to create the same LAG port with the same physical ports inside" - northboundV2.createLagLogicalPort(sw.dpId, new CreateLagPortDto(portNumbers: conflictPortsArray)) + northboundV2.createLagLogicalPort(sw.dpId, new LagPortRequest(portNumbers: conflictPortsArray)) then: "Human readable error is returned" def exc = thrown(HttpClientErrorException) @@ -433,7 +433,7 @@ class LagPortSpec extends HealthCheckSpecification { given: "A switch with a LAG port" def sw = topology.getActiveSwitches().first() def portsArray = topology.getAllowedPortsForSwitch(sw)[-2,-1] - def payload = new CreateLagPortDto(portNumbers: portsArray) + def payload = new LagPortRequest(portNumbers: portsArray) def lagPort = northboundV2.createLagLogicalPort(sw.dpId, payload).logicalPortNumber when: "Delete LAG port via grpc" @@ -464,7 +464,7 @@ class LagPortSpec extends HealthCheckSpecification { given: "A switch with a LAG port" def sw = topology.getActiveSwitches().first() def portsArray = topology.getAllowedPortsForSwitch(sw)[-3,-1] - def payload = new CreateLagPortDto(portNumbers: portsArray) + def payload = new LagPortRequest(portNumbers: portsArray) def lagPort = northboundV2.createLagLogicalPort(sw.dpId, payload).logicalPortNumber when: "Modify LAG port via grpc(delete, create with incorrect ports)" 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 2755ffc80d1..6cecaee63c2 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 @@ -29,8 +29,8 @@ import org.openkilda.northbound.dto.v2.flows.FlowResponseV2; import org.openkilda.northbound.dto.v2.links.BfdProperties; import org.openkilda.northbound.dto.v2.links.BfdPropertiesPayload; -import org.openkilda.northbound.dto.v2.switches.CreateLagPortDto; -import org.openkilda.northbound.dto.v2.switches.LagPortDto; +import org.openkilda.northbound.dto.v2.switches.LagPortRequest; +import org.openkilda.northbound.dto.v2.switches.LagPortResponse; import org.openkilda.northbound.dto.v2.switches.PortHistoryResponse; import org.openkilda.northbound.dto.v2.switches.PortPropertiesDto; import org.openkilda.northbound.dto.v2.switches.PortPropertiesResponse; @@ -118,11 +118,11 @@ public interface NorthboundServiceV2 { SwitchPropertiesDump getAllSwitchProperties(); - List getLagLogicalPort(SwitchId switchId); + List getLagLogicalPort(SwitchId switchId); - LagPortDto createLagLogicalPort(SwitchId switchId, CreateLagPortDto payload); + LagPortResponse createLagLogicalPort(SwitchId switchId, LagPortRequest payload); - LagPortDto deleteLagLogicalPort(SwitchId switchId, Integer logicalPortNumber); + LagPortResponse deleteLagLogicalPort(SwitchId switchId, Integer logicalPortNumber); //links BfdPropertiesPayload setLinkBfd(TopologyDefinition.Isl isl); 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 880cb7e1e91..0a69eb38f65 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 @@ -30,8 +30,8 @@ import org.openkilda.northbound.dto.v2.flows.FlowResponseV2; import org.openkilda.northbound.dto.v2.links.BfdProperties; import org.openkilda.northbound.dto.v2.links.BfdPropertiesPayload; -import org.openkilda.northbound.dto.v2.switches.CreateLagPortDto; -import org.openkilda.northbound.dto.v2.switches.LagPortDto; +import org.openkilda.northbound.dto.v2.switches.LagPortRequest; +import org.openkilda.northbound.dto.v2.switches.LagPortResponse; import org.openkilda.northbound.dto.v2.switches.PortHistoryResponse; import org.openkilda.northbound.dto.v2.switches.PortPropertiesDto; import org.openkilda.northbound.dto.v2.switches.PortPropertiesResponse; @@ -312,26 +312,26 @@ public SwitchPropertiesDump getAllSwitchProperties() { } @Override - public List getLagLogicalPort(SwitchId switchId) { + public List getLagLogicalPort(SwitchId switchId) { log.debug("Get LAG ports from switch('{}')", switchId); - LagPortDto[] lagPorts = restTemplate.exchange("/api/v2/switches/{switchId}/lags", HttpMethod.GET, - new HttpEntity(buildHeadersWithCorrelationId()), LagPortDto[].class, switchId).getBody(); + LagPortResponse[] lagPorts = restTemplate.exchange("/api/v2/switches/{switchId}/lags", HttpMethod.GET, + new HttpEntity(buildHeadersWithCorrelationId()), LagPortResponse[].class, switchId).getBody(); return Arrays.asList(lagPorts); } @Override - public LagPortDto createLagLogicalPort(SwitchId switchId, CreateLagPortDto payload) { + public LagPortResponse createLagLogicalPort(SwitchId switchId, LagPortRequest payload) { log.debug("Create LAG port on switch('{}')", switchId); - HttpEntity httpEntity = new HttpEntity<>(payload, buildHeadersWithCorrelationId()); + HttpEntity httpEntity = new HttpEntity<>(payload, buildHeadersWithCorrelationId()); return restTemplate.exchange("/api/v2/switches/{switchId}/lags", HttpMethod.POST, httpEntity, - LagPortDto.class, switchId).getBody(); + LagPortResponse.class, switchId).getBody(); } @Override - public LagPortDto deleteLagLogicalPort(SwitchId switchId, Integer logicalPortNumber) { + public LagPortResponse deleteLagLogicalPort(SwitchId switchId, Integer logicalPortNumber) { log.debug("Delete LAG port('{}') from switch('{}')", logicalPortNumber, switchId); return restTemplate.exchange("/api/v2/switches/{switch_id}/lags/{logical_port_number}", HttpMethod.DELETE, - new HttpEntity<>(buildHeadersWithCorrelationId()), LagPortDto.class, switchId, + new HttpEntity<>(buildHeadersWithCorrelationId()), LagPortResponse.class, switchId, logicalPortNumber).getBody(); } From d4d0255e6472cf2cf6ebf967037f97d142311e1d Mon Sep 17 00:00:00 2001 From: Dmitry Poltavets Date: Mon, 21 Feb 2022 16:03:44 +0400 Subject: [PATCH 08/11] Added inaccurate flag to MeterSpeakerData. --- .../rulemanager/factory/MeteredRuleGenerator.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src-java/rule-manager/rule-manager-implementation/src/main/java/org/openkilda/rulemanager/factory/MeteredRuleGenerator.java b/src-java/rule-manager/rule-manager-implementation/src/main/java/org/openkilda/rulemanager/factory/MeteredRuleGenerator.java index 69ae25488c1..bf204b5ac05 100644 --- a/src-java/rule-manager/rule-manager-implementation/src/main/java/org/openkilda/rulemanager/factory/MeteredRuleGenerator.java +++ b/src-java/rule-manager/rule-manager-implementation/src/main/java/org/openkilda/rulemanager/factory/MeteredRuleGenerator.java @@ -15,6 +15,7 @@ package org.openkilda.rulemanager.factory; +import static org.openkilda.model.SwitchFeature.INACCURATE_METER; import static org.openkilda.model.SwitchFeature.METERS; import static org.openkilda.rulemanager.factory.generator.flow.IngressRuleGenerator.FLOW_METER_STATS; @@ -22,6 +23,7 @@ import org.openkilda.model.Meter; import org.openkilda.model.MeterId; import org.openkilda.model.Switch; +import org.openkilda.model.SwitchFeature; import org.openkilda.rulemanager.Instructions; import org.openkilda.rulemanager.MeterSpeakerData; import org.openkilda.rulemanager.OfVersion; @@ -29,6 +31,7 @@ import org.openkilda.rulemanager.SpeakerData; import org.openkilda.rulemanager.action.MeterAction; +import java.util.Set; import java.util.UUID; public interface MeteredRuleGenerator extends RuleGenerator { @@ -61,7 +64,8 @@ default void addMeterToInstructions(MeterId meterId, Switch sw, Instructions ins */ default SpeakerData buildMeter(UUID uuid, FlowPath flowPath, RuleManagerConfig config, MeterId meterId, Switch sw) { - if (meterId == null || !sw.getFeatures().contains(METERS)) { + Set switchFeatures = sw.getFeatures(); + if (meterId == null || !switchFeatures.contains(METERS)) { return null; } @@ -78,6 +82,7 @@ default SpeakerData buildMeter(UUID uuid, FlowPath flowPath, RuleManagerConfig c .rate(flowPath.getBandwidth()) .burst(burstSize) .flags(FLOW_METER_STATS) + .inaccurate(switchFeatures.contains(INACCURATE_METER)) .build(); } From 03b4783505c3274231af15574b0d110eb9a91103 Mon Sep 17 00:00:00 2001 From: Sergey Nikitin Date: Mon, 21 Feb 2022 14:41:32 +0300 Subject: [PATCH 09/11] Added SLA check sharding for Flow Monitoring --- .../topology.properties.tmpl | 6 +++ confd/vars/main.yaml | 1 + .../FlowMonitoringTopology.java | 6 ++- .../FlowMonitoringTopologyConfig.java | 10 ++++ .../flowmonitoring/bolt/ActionBolt.java | 19 +++++-- .../bolt/FlowStateCacheBolt.java | 9 +++- .../flowmonitoring/bolt/TickBolt.java | 16 ++++-- .../flowmonitoring/service/ActionService.java | 49 +++++++++++++------ .../service/ActionServiceTest.java | 17 ++++--- 9 files changed, 97 insertions(+), 36 deletions(-) diff --git a/confd/templates/base-storm-topology/topology.properties.tmpl b/confd/templates/base-storm-topology/topology.properties.tmpl index 4832603d996..0ed67b9cfdd 100644 --- a/confd/templates/base-storm-topology/topology.properties.tmpl +++ b/confd/templates/base-storm-topology/topology.properties.tmpl @@ -162,7 +162,13 @@ flow.delete.speaker.command.retries = 3 blue.green.mode = {{ getv "/kilda_blue_green_mode" "blue" }} # flow-monitoring topology +# +# flow.sla.check.shard.count parameter is needed to distribute load of Flow SLA +# check operation. Instead of checking of all flows ones in flow.sla.check.interval.seconds +# flows will be checked by chunks. That is why parameter flow.sla.check.interval.seconds +# must be divisible by flow.sla.check.shard.count flow.sla.check.interval.seconds = {{ getv "/kilda_flow_sla_check_interval_seconds" }} +flow.sla.check.shard.count = {{ getv "/kilda_flow_sla_check_shard_count" }} flow.rtt.stats.expiration.seconds = {{ getv "/kilda_flow_rtt_stats_expiration_seconds" }} isl.rtt.latency.expiration.seconds = {{ getv "/kilda_isl_rtt_latency_expiration_seconds" }} flow.latency.sla.timeout.seconds = {{ getv "/kilda_flow_latency_sla_timeout_seconds" }} diff --git a/confd/vars/main.yaml b/confd/vars/main.yaml index 2348695899d..c9b7f74f387 100644 --- a/confd/vars/main.yaml +++ b/confd/vars/main.yaml @@ -209,6 +209,7 @@ kilda_server42_control_storm_stub_component_name: "server42-control-storm-stub" kilda_server42_control_storm_stub_run_id: "server42-control-storm-stub-run-id" kilda_flow_sla_check_interval_seconds: 60 +kilda_flow_sla_check_shard_count: 10 kilda_flow_rtt_stats_expiration_seconds: 30 kilda_isl_rtt_latency_expiration_seconds: 10 kilda_flow_latency_sla_timeout_seconds: 30 diff --git a/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/FlowMonitoringTopology.java b/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/FlowMonitoringTopology.java index 8b66dd31b3b..ef4c149babd 100644 --- a/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/FlowMonitoringTopology.java +++ b/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/FlowMonitoringTopology.java @@ -117,7 +117,8 @@ private void flowSplitterBolt(TopologyBuilder topologyBuilder) { } private void tickBolt(TopologyBuilder topologyBuilder) { - declareBolt(topologyBuilder, new TickBolt(getConfig().getFlowSlaCheckIntervalSeconds()), + declareBolt(topologyBuilder, new TickBolt(getConfig().getFlowSlaCheckIntervalSeconds(), + getConfig().getFlowSlaCheckIntervalSeconds() / getConfig().getFlowSlaCheckShardCount()), ComponentId.TICK_BOLT.name()); } @@ -156,7 +157,8 @@ private void islCacheBolt(TopologyBuilder topologyBuilder, PersistenceManager pe private void actionBolt(TopologyBuilder topologyBuilder, PersistenceManager persistenceManager) { declareBolt(topologyBuilder, new ActionBolt(persistenceManager, Duration.ofSeconds(getConfig().getFlowLatencySlaTimeoutSeconds()), - getConfig().getFlowLatencySlaThresholdPercent(), ZooKeeperSpout.SPOUT_ID), + getConfig().getFlowLatencySlaThresholdPercent(), ZooKeeperSpout.SPOUT_ID, + getConfig().getFlowSlaCheckShardCount()), ComponentId.ACTION_BOLT.name()) .fieldsGrouping(ComponentId.FLOW_CACHE_BOLT.name(), ACTION_STREAM_ID.name(), FLOW_ID_FIELDS) .fieldsGrouping(ComponentId.FLOW_CACHE_BOLT.name(), FLOW_UPDATE_STREAM_ID.name(), FLOW_ID_FIELDS) diff --git a/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/FlowMonitoringTopologyConfig.java b/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/FlowMonitoringTopologyConfig.java index a9bcede1b0f..c465e256cc0 100644 --- a/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/FlowMonitoringTopologyConfig.java +++ b/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/FlowMonitoringTopologyConfig.java @@ -49,6 +49,16 @@ default String getKafkaTopoRerouteTopic() { @Default("30") int getFlowSlaCheckIntervalSeconds(); + /* + * flow.sla.check.shard.count parameter is needed to distribute load of Flow SLA + * check operation. Instead of checking of all flows ones in flow.sla.check.interval.seconds + * flows will be checked by chunks. That is why parameter flow.sla.check.interval.seconds + * must be divisible by flow.sla.check.shard.count + */ + @Key("flow.sla.check.shard.count") + @Default("10") + int getFlowSlaCheckShardCount(); + @Key("flow.rtt.stats.expiration.seconds") @Default("3") int getFlowRttStatsExpirationSeconds(); diff --git a/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/bolt/ActionBolt.java b/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/bolt/ActionBolt.java index 31bd56c0882..928faeab3ab 100644 --- a/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/bolt/ActionBolt.java +++ b/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/bolt/ActionBolt.java @@ -16,6 +16,7 @@ package org.openkilda.wfm.topology.flowmonitoring.bolt; import static org.openkilda.wfm.share.bolt.KafkaEncoder.FIELD_ID_PAYLOAD; +import static org.openkilda.wfm.share.bolt.MonotonicClock.FIELD_ID_TICK_IDENTIFIER; import static org.openkilda.wfm.topology.flowmonitoring.FlowMonitoringTopology.Stream.ACTION_STREAM_ID; import static org.openkilda.wfm.topology.flowmonitoring.FlowMonitoringTopology.Stream.FLOW_REMOVE_STREAM_ID; import static org.openkilda.wfm.topology.flowmonitoring.FlowMonitoringTopology.Stream.FLOW_UPDATE_STREAM_ID; @@ -47,22 +48,26 @@ public class ActionBolt extends AbstractBolt implements FlowOperationsCarrier { - private Duration timeout; - private float threshold; + private final Duration timeout; + private final float threshold; + private final int shardCount; + private int currentShardNumber; private transient ActionService actionService; public ActionBolt( PersistenceManager persistenceManager, Duration timeout, float threshold, - String lifeCycleEventSourceComponent) { + String lifeCycleEventSourceComponent, int shardCount) { super(persistenceManager, lifeCycleEventSourceComponent); this.timeout = timeout; this.threshold = threshold; + this.currentShardNumber = 0; + this.shardCount = shardCount; } @Override protected void init() { super.init(); - actionService = new ActionService(this, persistenceManager, Clock.systemUTC(), timeout, threshold); + actionService = new ActionService(this, persistenceManager, Clock.systemUTC(), timeout, threshold, shardCount); } @Override @@ -91,7 +96,11 @@ protected void handleInput(Tuple input) throws PipelineException { } if (ComponentId.TICK_BOLT.name().equals(input.getSourceComponent())) { - actionService.processTick(); + TickBolt.TickId tickId = pullValue(input, FIELD_ID_TICK_IDENTIFIER, TickBolt.TickId.class); + if (TickBolt.TickId.SLA_CHECK.equals(tickId)) { + actionService.processTick(currentShardNumber); + currentShardNumber = (currentShardNumber + 1) % shardCount; + } } else { unhandledInput(input); } diff --git a/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/bolt/FlowStateCacheBolt.java b/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/bolt/FlowStateCacheBolt.java index 3444759e6c6..911c38cbb82 100644 --- a/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/bolt/FlowStateCacheBolt.java +++ b/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/bolt/FlowStateCacheBolt.java @@ -15,6 +15,7 @@ package org.openkilda.wfm.topology.flowmonitoring.bolt; +import static org.openkilda.wfm.share.bolt.MonotonicClock.FIELD_ID_TICK_IDENTIFIER; import static org.openkilda.wfm.topology.flowmonitoring.FlowMonitoringTopology.Stream.FLOW_REMOVE_STREAM_ID; import static org.openkilda.wfm.topology.flowmonitoring.FlowMonitoringTopology.Stream.FLOW_UPDATE_STREAM_ID; import static org.openkilda.wfm.topology.flowmonitoring.bolt.FlowCacheBolt.FLOW_ID_FIELD; @@ -29,6 +30,7 @@ import org.openkilda.wfm.share.zk.ZkStreams; import org.openkilda.wfm.share.zk.ZooKeeperBolt; import org.openkilda.wfm.topology.flowmonitoring.FlowMonitoringTopology.ComponentId; +import org.openkilda.wfm.topology.flowmonitoring.bolt.TickBolt.TickId; import org.openkilda.wfm.topology.flowmonitoring.service.FlowStateCacheService; import org.apache.storm.topology.OutputFieldsDeclarer; @@ -52,8 +54,11 @@ protected void init() { protected void handleInput(Tuple input) throws PipelineException { if (active) { if (ComponentId.TICK_BOLT.name().equals(input.getSourceComponent())) { - flowStateCacheService.getFlows() - .forEach(flowId -> emit(input, new Values(flowId, getCommandContext()))); + TickId tickId = pullValue(input, FIELD_ID_TICK_IDENTIFIER, TickId.class); + if (TickId.CACHE_UPDATE.equals(tickId)) { + flowStateCacheService.getFlows() + .forEach(flowId -> emit(input, new Values(flowId, getCommandContext()))); + } return; } diff --git a/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/bolt/TickBolt.java b/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/bolt/TickBolt.java index 17f0290eeb7..71cf1ad2bca 100644 --- a/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/bolt/TickBolt.java +++ b/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/bolt/TickBolt.java @@ -19,9 +19,19 @@ import org.openkilda.wfm.topology.flowmonitoring.bolt.TickBolt.TickId; public class TickBolt extends MonotonicClock { - public TickBolt(Integer interval) { - super(new MonotonicClock.ClockConfig<>(), interval); + public TickBolt(int cacheCheckInterval, int slaCheckInterval) { + super(createConfig(cacheCheckInterval, slaCheckInterval)); } - enum TickId {} + private static ClockConfig createConfig(int cacheCheckInterval, int slaCheckInterval) { + ClockConfig config = new ClockConfig<>(); + config.addTickInterval(TickId.CACHE_UPDATE, cacheCheckInterval); + config.addTickInterval(TickId.SLA_CHECK, slaCheckInterval); + return config; + } + + enum TickId { + CACHE_UPDATE, + SLA_CHECK + } } diff --git a/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/service/ActionService.java b/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/service/ActionService.java index e248924ff57..901095dc171 100644 --- a/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/service/ActionService.java +++ b/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/main/java/org/openkilda/wfm/topology/flowmonitoring/service/ActionService.java @@ -40,6 +40,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Sets; +import lombok.Value; import lombok.extern.slf4j.Slf4j; import java.time.Clock; @@ -55,21 +56,21 @@ public class ActionService implements FlowSlaMonitoringCarrier { private static final Set LATENCY_BASED_STRATEGIES = Sets.newHashSet(PathComputationStrategy.LATENCY, PathComputationStrategy.MAX_LATENCY); - private FlowOperationsCarrier carrier; - private FlowRepository flowRepository; - private FlowStatsRepository flowStatsRepository; - private KildaFeatureTogglesRepository featureTogglesRepository; - private TransactionManager transactionManager; - private FlowLatencyMonitoringFsmFactory fsmFactory; - private FsmExecutor fsmExecutor; + private final FlowOperationsCarrier carrier; + private final FlowRepository flowRepository; + private final FlowStatsRepository flowStatsRepository; + private final KildaFeatureTogglesRepository featureTogglesRepository; + private final TransactionManager transactionManager; + private final FlowLatencyMonitoringFsmFactory fsmFactory; + private final FsmExecutor fsmExecutor; - private float threshold; + private final int shardCount; @VisibleForTesting - protected Map fsms = new HashMap<>(); + protected Map fsms = new HashMap<>(); public ActionService(FlowOperationsCarrier carrier, PersistenceManager persistenceManager, - Clock clock, Duration timeout, float threshold) { + Clock clock, Duration timeout, float threshold, int shardCount) { this.carrier = carrier; flowRepository = persistenceManager.getRepositoryFactory().createFlowRepository(); flowStatsRepository = persistenceManager.getRepositoryFactory().createFlowStatsRepository(); @@ -77,7 +78,7 @@ public ActionService(FlowOperationsCarrier carrier, PersistenceManager persisten transactionManager = persistenceManager.getTransactionManager(); fsmFactory = FlowLatencyMonitoringFsm.factory(clock, timeout, threshold); fsmExecutor = fsmFactory.produceExecutor(); - this.threshold = threshold; + this.shardCount = shardCount; } /** @@ -103,7 +104,7 @@ public void removeFlowInfo(String flowId) { * Check flow SLA is violated. */ public void processFlowLatencyMeasurement(String flowId, FlowDirection direction, Duration latency) { - String key = getFsmKey(flowId, direction); + FsmKey key = getFsmKey(flowId, direction); FlowLatencyMonitoringFsm fsm = fsms.get(key); if (fsm == null) { Flow flow = flowRepository.findById(flowId) @@ -126,15 +127,25 @@ public void processFlowLatencyMeasurement(String flowId, FlowDirection direction /** * Process tick. */ - public void processTick() { + public void processTick(int shardNumber) { Context context = Context.builder() .carrier(this) .build(); - fsms.values().forEach(fsm -> fsmExecutor.fire(fsm, Event.TICK, context)); + if (log.isDebugEnabled()) { + log.debug("Processing flow SLA checks for shard {}", shardNumber); + } + for (FsmKey key : fsms.keySet()) { + if (key.flowId.hashCode() % shardCount == shardNumber) { + if (log.isTraceEnabled()) { + log.trace("Processing SLA check for flow FSM {}: Shard number: {}", key, shardNumber); + } + fsmExecutor.fire(fsms.get(key), Event.TICK, context); + } + } } - private String getFsmKey(String flowId, FlowDirection direction) { - return format("%s_%s", flowId, direction.name().toLowerCase()); + private FsmKey getFsmKey(String flowId, FlowDirection direction) { + return new FsmKey(flowId, direction); } /** @@ -200,4 +211,10 @@ public void sendFlowRerouteRequest(String flowId) { private boolean isReactionsEnabled() { return featureTogglesRepository.getOrDefault().getFlowLatencyMonitoringReactions(); } + + @Value + private static class FsmKey { + String flowId; + FlowDirection direction; + } } diff --git a/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/test/java/org/openkilda/wfm/topology/flowmonitoring/service/ActionServiceTest.java b/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/test/java/org/openkilda/wfm/topology/flowmonitoring/service/ActionServiceTest.java index 35c2e7bc0f2..38df6fe531b 100644 --- a/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/test/java/org/openkilda/wfm/topology/flowmonitoring/service/ActionServiceTest.java +++ b/src-java/flowmonitoring-topology/flowmonitoring-storm-topology/src/test/java/org/openkilda/wfm/topology/flowmonitoring/service/ActionServiceTest.java @@ -66,6 +66,7 @@ public class ActionServiceTest extends InMemoryGraphBasedTest { private static final Duration TIMEOUT = Duration.ofSeconds(30); private static final float THRESHOLD = 0.1f; + public static final int SHARD_COUNT = 1; private PersistenceDummyEntityFactory dummyFactory; private FlowRepository flowRepository; @@ -95,7 +96,7 @@ public void setup() { flow = dummyFactory.makeFlow(new FlowEndpoint(SRC_SWITCH, IN_PORT), new FlowEndpoint(DST_SWITCH, OUT_PORT)); - service = new ActionService(carrier, persistenceManager, clock, TIMEOUT, THRESHOLD); + service = new ActionService(carrier, persistenceManager, clock, TIMEOUT, THRESHOLD, SHARD_COUNT); } @Test @@ -111,7 +112,7 @@ public void shouldStayInHealthyState() { clock.adjust(Duration.ofSeconds(10)); service.processFlowLatencyMeasurement(flow.getFlowId(), FlowDirection.FORWARD, latency); service.processFlowLatencyMeasurement(flow.getFlowId(), FlowDirection.REVERSE, latency.minus(NANOSECOND)); - service.processTick(); + service.processTick(0); } assertEquals(2, service.fsms.values().size()); @@ -135,7 +136,7 @@ public void shouldFailTier1AndSendRerouteRequest() { clock.adjust(Duration.ofSeconds(10)); service.processFlowLatencyMeasurement(flow.getFlowId(), FlowDirection.FORWARD, latency); service.processFlowLatencyMeasurement(flow.getFlowId(), FlowDirection.REVERSE, latency.minus(NANOSECOND)); - service.processTick(); + service.processTick(0); if (i == 0) { assertTrue(service.fsms.values().stream().allMatch(fsm -> UNSTABLE.equals(fsm.getCurrentState()))); } @@ -169,7 +170,7 @@ public void shouldFailTier1AndDoNotSendRerouteRequestWhenToggleIsFalse() { clock.adjust(Duration.ofSeconds(10)); service.processFlowLatencyMeasurement(flow.getFlowId(), FlowDirection.FORWARD, latency); service.processFlowLatencyMeasurement(flow.getFlowId(), FlowDirection.REVERSE, latency.minus(NANOSECOND)); - service.processTick(); + service.processTick(0); if (i == 0) { assertTrue(service.fsms.values().stream().allMatch(fsm -> UNSTABLE.equals(fsm.getCurrentState()))); } @@ -202,7 +203,7 @@ public void shouldFailTier1AndDoNotSendRerouteRequestForCostStrategy() { clock.adjust(Duration.ofSeconds(10)); service.processFlowLatencyMeasurement(flow.getFlowId(), FlowDirection.FORWARD, latency); service.processFlowLatencyMeasurement(flow.getFlowId(), FlowDirection.REVERSE, latency.minus(NANOSECOND)); - service.processTick(); + service.processTick(0); if (i == 0) { assertTrue(service.fsms.values().stream().allMatch(fsm -> UNSTABLE.equals(fsm.getCurrentState()))); } @@ -235,7 +236,7 @@ public void shouldFailTier2AndSendRerouteRequest() { clock.adjust(Duration.ofSeconds(10)); service.processFlowLatencyMeasurement(flow.getFlowId(), FlowDirection.FORWARD, latency); service.processFlowLatencyMeasurement(flow.getFlowId(), FlowDirection.REVERSE, latency.minus(NANOSECOND)); - service.processTick(); + service.processTick(0); if (i == 0) { assertTrue(service.fsms.values().stream().allMatch(fsm -> UNSTABLE.equals(fsm.getCurrentState()))); } @@ -269,7 +270,7 @@ public void shouldFailTier2AndDoNotSendRerouteRequestForCostStrategy() { clock.adjust(Duration.ofSeconds(10)); service.processFlowLatencyMeasurement(flow.getFlowId(), FlowDirection.FORWARD, latency); service.processFlowLatencyMeasurement(flow.getFlowId(), FlowDirection.REVERSE, latency.minus(NANOSECOND)); - service.processTick(); + service.processTick(0); if (i == 0) { assertTrue(service.fsms.values().stream().allMatch(fsm -> UNSTABLE.equals(fsm.getCurrentState()))); } @@ -297,7 +298,7 @@ public void shouldBecomeHealthyAndSendSyncRequest() { clock.adjust(Duration.ofSeconds(10)); service.processFlowLatencyMeasurement(flow.getFlowId(), FlowDirection.FORWARD, healthy); service.processFlowLatencyMeasurement(flow.getFlowId(), FlowDirection.REVERSE, healthy.minus(NANOSECOND)); - service.processTick(); + service.processTick(0); if (i == 0) { assertTrue(service.fsms.values().stream().allMatch(fsm -> UNSTABLE.equals(fsm.getCurrentState()))); } From fe032c5fcb854dd55e3f5c4369ffa64475c4d1c4 Mon Sep 17 00:00:00 2001 From: Dmitry Poltavets Date: Fri, 25 Feb 2022 13:27:50 +0400 Subject: [PATCH 10/11] Fix unmetered bw for noviflow switches --- .../openkilda/wfm/topology/flowhs/validation/FlowValidator.java | 2 +- .../wfm/topology/flowhs/validation/YFlowValidator.java | 2 +- 2 files changed, 2 insertions(+), 2 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 b70e2884834..adc6338cf75 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 @@ -221,7 +221,7 @@ void checkBandwidth(RequestedFlow flow) throws InvalidFlowException, Unavailable if ((Switch.isNoviflowSwitch(srcSwitch.getOfDescriptionSoftware()) || Switch.isNoviflowSwitch(destSwitch.getOfDescriptionSoftware())) - && flow.getBandwidth() < 64) { + && flow.getBandwidth() != 0 && flow.getBandwidth() < 64) { // Min rate that the NoviFlow switches allows is 64 kbps. throw new InvalidFlowException( format("The flow '%s' has invalid bandwidth %d provided. Bandwidth cannot be less than 64 kbps.", diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/validation/YFlowValidator.java b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/validation/YFlowValidator.java index 335b5d29541..328aca49c24 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/validation/YFlowValidator.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/validation/YFlowValidator.java @@ -153,7 +153,7 @@ private void checkBandwidth(YFlowRequest yFlowRequest) isNoviFlowSwitch |= Switch.isNoviflowSwitch(switchId.getOfDescriptionSoftware()); } - if (isNoviFlowSwitch && yFlowRequest.getMaximumBandwidth() < 64) { + if (isNoviFlowSwitch && yFlowRequest.getMaximumBandwidth() != 0 && yFlowRequest.getMaximumBandwidth() < 64) { // Min rate that the NoviFlow switches allows is 64 kbps. throw new InvalidFlowException( format("The flow '%s' has invalid bandwidth %d provided. Bandwidth cannot be less than 64 kbps.", From b227a47e6314481c0d49401a45fc2ffaf84a1910 Mon Sep 17 00:00:00 2001 From: Dmitry Poltavets Date: Fri, 25 Feb 2022 13:37:17 +0400 Subject: [PATCH 11/11] Update CHANGELOG.md --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d5165b7c52..6163fc3b9ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## v1.116.0 (04/03/2022) + +### Features: +- [#4698](https://github.com/telstra/open-kilda/pull/4698) Added sync switch on connect toggle [**storm-topologies**] + +### Bug Fixes: +- [#4709](https://github.com/telstra/open-kilda/pull/4709) Fix: add inaccurate flag to MeterSpeakerData. +- [#4714](https://github.com/telstra/open-kilda/pull/4714) Fix unmetered bw for noviflow switches +- [#4444](https://github.com/telstra/open-kilda/pull/4444) GRPC: Fixed changing of logical port type (Issues: [#4693](https://github.com/telstra/open-kilda/issues/4693) [#4694](https://github.com/telstra/open-kilda/issues/4694)) +- [#4701](https://github.com/telstra/open-kilda/pull/4701) Fix the minimum bandwidth value for the flow +- [#4702](https://github.com/telstra/open-kilda/pull/4702) Fix excess y-flow meters (proper building & handing of DeleteSpeakerCommandsRequest) [**floodlight**] + +### Improvements: +- [#4680](https://github.com/telstra/open-kilda/pull/4680) LAG logical port update operation +- [#4699](https://github.com/telstra/open-kilda/pull/4699) Rework events routing inside SwitchManagerHub [**storm-topologies**] +- [#4700](https://github.com/telstra/open-kilda/pull/4700) Ignore leading and trailing spaces in a string for the SwitchId class. + + +For the complete list of changes, check out [the commit log](https://github.com/telstra/open-kilda/compare/v1.115.2...v1.116.0). + +### Affected Components: +flow-monitor, network, grpc, flow-hs, swmanager, fl + +--- + ## v1.115.2 (02/03/2022) ### Improvements: