Skip to content

Commit 801a2ef

Browse files
authored
Merge pull request #624 from kytos-ng/release/2023.2.10
release 2023.2.10: fixed request timeout that could indefinitely hang + backported vlan path choose vlan fix
2 parents 66172e0 + d2ef073 commit 801a2ef

File tree

6 files changed

+108
-17
lines changed

6 files changed

+108
-17
lines changed

CHANGELOG.rst

+9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ All notable changes to the MEF_ELine NApp will be documented in this file.
66
[Unreleased]
77
************
88

9+
[2023.2.10] - 2025-02-12
10+
************************
11+
12+
Fixed
13+
=====
14+
- Parametrized requests timeout when finding paths and when sending flow mods to avoid potentially hanging indefinitely
15+
- Fixed Path ``choose_vlans`` to be all or nothing, if a path link fails to allocate a vlan, it'll release the allocated vlans.
16+
17+
918
[2023.2.9] - 2025-01-22
1019
***********************
1120

kytos.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"username": "kytos",
44
"name": "mef_eline",
55
"description": "NApp to provision circuits from user request",
6-
"version": "2023.2.9",
6+
"version": "2023.2.10",
77
"napp_dependencies": ["kytos/flow_manager", "kytos/pathfinder", "amlight/sndtrace_cp"],
88
"license": "MIT",
99
"tags": [],

models/evc.py

+17-2
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@
1111
import requests
1212
from glom import glom
1313
from requests.exceptions import Timeout
14+
from tenacity import (retry, retry_if_exception_type, stop_after_attempt,
15+
wait_combine, wait_fixed, wait_random)
1416

1517
from kytos.core import log
1618
from kytos.core.common import EntityStatus, GenericEntity
1719
from kytos.core.exceptions import KytosNoTagAvailableError, KytosTagError
1820
from kytos.core.helpers import get_time, now
1921
from kytos.core.interface import UNI, Interface, TAGRange
2022
from kytos.core.link import Link
23+
from kytos.core.retry import before_sleep
2124
from kytos.core.tag_ranges import range_difference
2225
from napps.kytos.mef_eline import controllers, settings
2326
from napps.kytos.mef_eline.exceptions import (ActivationError,
@@ -1270,21 +1273,33 @@ def _install_uni_flows(self, path=None, skip_in=False, skip_out=False):
12701273
self._send_flow_mods(dpid, flows)
12711274

12721275
@staticmethod
1273-
def _send_flow_mods(dpid, flow_mods, command='flows', force=False):
1276+
@retry(
1277+
stop=stop_after_attempt(3),
1278+
wait=wait_combine(wait_fixed(3), wait_random(min=0, max=5)),
1279+
retry=retry_if_exception_type(FlowModException),
1280+
before_sleep=before_sleep,
1281+
reraise=True
1282+
)
1283+
def _send_flow_mods(dpid, flow_mods, command='flows', force=False,
1284+
timeout=30):
12741285
"""Send a flow_mod list to a specific switch.
12751286
12761287
Args:
12771288
dpid(str): The target of flows (i.e. Switch.id).
12781289
flow_mods(dict): Python dictionary with flow_mods.
12791290
command(str): By default is 'flows'. To remove a flow is 'remove'.
12801291
force(bool): True to send via consistency check in case of errors
1292+
timeout(int): request timeout
12811293
12821294
"""
12831295

12841296
endpoint = f"{settings.MANAGER_URL}/{command}/{dpid}"
12851297

12861298
data = {"flows": flow_mods, "force": force}
1287-
response = requests.post(endpoint, json=data)
1299+
try:
1300+
response = requests.post(endpoint, json=data, timeout=timeout)
1301+
except requests.exceptions.RequestException as exc:
1302+
raise FlowModException(str(exc)) from exc
12881303
if response.status_code >= 400:
12891304
raise FlowModException(str(response.text))
12901305

models/path.py

+39-12
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
"""Classes related to paths"""
22
import requests
3+
from tenacity import (retry, retry_if_exception_type, stop_after_attempt,
4+
wait_combine, wait_fixed, wait_random)
35

46
from kytos.core import log
57
from kytos.core.common import EntityStatus, GenericEntity
8+
from kytos.core.exceptions import KytosNoTagAvailableError
69
from kytos.core.interface import TAG
710
from kytos.core.link import Link
11+
from kytos.core.retry import before_sleep
812
from napps.kytos.mef_eline import settings
913
from napps.kytos.mef_eline.exceptions import InvalidPath
1014

@@ -34,20 +38,29 @@ def link_affected_by_interface(self, interface=None):
3438
return None
3539

3640
def choose_vlans(self, controller, old_path_dict: dict = None):
37-
"""Choose the VLANs to be used for the circuit."""
41+
"""Choose the VLANs to be used for the circuit.
42+
If any of the links next tag isn't available, it'll release
43+
all vlans of the path that have been allocated, all or nothing.
44+
"""
3845
old_path_dict = old_path_dict if old_path_dict else {}
39-
for link in self:
40-
tag_value = link.get_next_available_tag(
41-
controller, link.id,
42-
try_avoid_value=old_path_dict.get(link.id)
43-
)
44-
tag = TAG('vlan', tag_value)
45-
link.add_metadata("s_vlan", tag)
46+
try:
47+
for link in self:
48+
tag_value = link.get_next_available_tag(
49+
controller, link.id,
50+
try_avoid_value=old_path_dict.get(link.id)
51+
)
52+
tag = TAG('vlan', tag_value)
53+
link.add_metadata("s_vlan", tag)
54+
except KytosNoTagAvailableError as exc:
55+
self.make_vlans_available(controller)
56+
raise exc
4657

4758
def make_vlans_available(self, controller):
4859
"""Make the VLANs used in a path available when undeployed."""
4960
for link in self:
5061
tag = link.get_metadata("s_vlan")
62+
if not tag:
63+
continue
5164
conflict_a, conflict_b = link.make_tags_available(
5265
controller, tag.value, link.id, tag.tag_type,
5366
check_order=False
@@ -126,7 +139,14 @@ def set_controller(cls, controller=None):
126139
cls.controller = controller
127140

128141
@staticmethod
129-
def get_paths(circuit, max_paths=2, **kwargs) -> list[dict]:
142+
@retry(
143+
stop=stop_after_attempt(3),
144+
wait=wait_combine(wait_fixed(3), wait_random(min=0, max=5)),
145+
retry=retry_if_exception_type(requests.exceptions.RequestException),
146+
before_sleep=before_sleep,
147+
reraise=True
148+
)
149+
def get_paths(circuit, max_paths=2, timeout=30, **kwargs) -> list[dict]:
130150
"""Get a valid path for the circuit from the Pathfinder."""
131151
endpoint = settings.PATHFINDER_URL
132152
spf_attribute = kwargs.get("spf_attribute") or settings.SPF_ATTRIBUTE
@@ -137,7 +157,7 @@ def get_paths(circuit, max_paths=2, **kwargs) -> list[dict]:
137157
"spf_attribute": spf_attribute
138158
}
139159
request_data.update(kwargs)
140-
api_reply = requests.post(endpoint, json=request_data)
160+
api_reply = requests.post(endpoint, timeout=timeout, json=request_data)
141161

142162
if api_reply.status_code != getattr(requests.codes, "ok"):
143163
log.error(
@@ -167,8 +187,15 @@ def get_best_path(cls, circuit):
167187
@classmethod
168188
def get_best_paths(cls, circuit, **kwargs):
169189
"""Return the best paths available for a circuit, if they exist."""
170-
for path in cls.get_paths(circuit, **kwargs):
171-
yield cls.create_path(path["hops"])
190+
try:
191+
for path in cls.get_paths(circuit, **kwargs):
192+
yield cls.create_path(path["hops"])
193+
except requests.exceptions.RequestException as err:
194+
log.error(
195+
f"{circuit} failed to get paths from pathfinder. Error {err}"
196+
)
197+
return
198+
yield
172199

173200
@classmethod
174201
def get_disjoint_paths(

tests/unit/models/test_evc_deploy.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def test_send_flow_mods_case1(self, requests_mock):
123123
expected_data = {"flows": flow_mods, "force": False}
124124
assert requests_mock.post.call_count == 1
125125
requests_mock.post.assert_called_once_with(
126-
expected_endpoint, json=expected_data
126+
expected_endpoint, json=expected_data, timeout=30
127127
)
128128

129129
@patch("napps.kytos.mef_eline.models.evc.requests")
@@ -142,7 +142,7 @@ def test_send_flow_mods_case2(self, requests_mock):
142142
expected_data = {"flows": flow_mods, "force": True}
143143
assert requests_mock.post.call_count == 1
144144
requests_mock.post.assert_called_once_with(
145-
expected_endpoint, json=expected_data
145+
expected_endpoint, json=expected_data, timeout=30
146146
)
147147

148148
@patch("napps.kytos.mef_eline.models.evc.requests")

tests/unit/models/test_path.py

+40
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pytest
55
from napps.kytos.mef_eline import settings
66

7+
from kytos.core.exceptions import KytosNoTagAvailableError
78
from kytos.core.common import EntityStatus
89
from kytos.core.link import Link
910
from kytos.core.switch import Switch
@@ -120,6 +121,43 @@ def test_compare_same_paths(self):
120121
path_2 = Path(links)
121122
assert path_1 == path_2
122123

124+
def test_choose_vlans(self) -> None:
125+
"""Test choose vlans."""
126+
controller = MagicMock()
127+
link1 = get_link_mocked(status=EntityStatus.UP)
128+
link2 = get_link_mocked(status=EntityStatus.UP)
129+
link1.id = "def"
130+
link2.id = "abc"
131+
links = [link1, link2]
132+
path = Path(links)
133+
path.make_vlans_available = MagicMock()
134+
path.choose_vlans(controller)
135+
assert link1.get_next_available_tag.call_count == 1
136+
assert link1.add_metadata.call_count == 1
137+
assert link2.get_next_available_tag.call_count == 1
138+
assert link2.add_metadata.call_count == 1
139+
assert not path.make_vlans_available.call_count
140+
141+
def test_choose_vlans_tags_not_available(self) -> None:
142+
"""Test choose vlans rollback if tags not available."""
143+
controller = MagicMock()
144+
link1 = get_link_mocked(status=EntityStatus.UP)
145+
link2 = get_link_mocked(status=EntityStatus.UP)
146+
link1.id = "def"
147+
link2.id = "abc"
148+
links = [link1, link2]
149+
path = Path(links)
150+
path.make_vlans_available = MagicMock()
151+
exc = KytosNoTagAvailableError(link2)
152+
link2.get_next_available_tag.side_effect = exc
153+
with pytest.raises(KytosNoTagAvailableError):
154+
path.choose_vlans(controller)
155+
assert link1.get_next_available_tag.call_count == 1
156+
assert link1.add_metadata.call_count == 1
157+
assert link2.get_next_available_tag.call_count == 1
158+
assert not link2.add_metadata.call_count
159+
assert path.make_vlans_available.call_count == 1
160+
123161
def test_compare_different_paths(self):
124162
"""Test compare paths with different links."""
125163
links_1 = [
@@ -416,6 +454,7 @@ def test_get_best_paths(self, mock_requests_post):
416454
)
417455
expected_call = call(
418456
"http://localhost:8181/api/kytos/pathfinder/v3/",
457+
timeout=30,
419458
json={
420459
**{
421460
"source": circuit.uni_a.interface.id,
@@ -667,6 +706,7 @@ def test_get_disjoint_paths(self, mock_requests_post, mock_shared):
667706
max_paths = 10
668707
expected_call = call(
669708
"http://localhost:8181/api/kytos/pathfinder/v3/",
709+
timeout=30,
670710
json={
671711
**{
672712
"source": evc.uni_a.interface.id,

0 commit comments

Comments
 (0)