Skip to content

Commit c64454c

Browse files
ghoooyxieca
authored andcommitted
[GCU] Moving UniqueLanes from only validating moves, to be a supplemental YANG validator (#2234)
#### What I did - Added a new supplemental YANG validator to validate UniqueLanes in `validate_config_db_config` - Removed UniqueLanesMoveValidator as the lanes validation will be taken care of by FullConfigMoveValidator which uses `validate_config_db_config` The benefit of this is at the beginning of `apply-patch` we make a call to `validate_config_db_config` to check if the given patch is valid or not ([code](https://github.com/Azure/sonic-utilities/blob/e6e4f8ceb9a59fb7b3767a65ffc4f017d0807832/generic_config_updater/patch_sorter.py#L1522)). Now we will fail early, instead of going for the move generation and not being able to generate a moves. #### How I did it Check code. #### How to verify it Added unit-tests. #### Previous command output (if the output of a command-line utility has changed) #### New command output (if the output of a command-line utility has changed)
1 parent fbd79d4 commit c64454c

File tree

4 files changed

+119
-71
lines changed

4 files changed

+119
-71
lines changed

generic_config_updater/gu_common.py

+41-2
Original file line numberDiff line numberDiff line change
@@ -113,18 +113,57 @@ def validate_sonic_yang_config(self, sonic_yang_as_json):
113113
def validate_config_db_config(self, config_db_as_json):
114114
sy = self.create_sonic_yang_with_loaded_models()
115115

116+
# TODO: Move these validators to YANG models
117+
supplemental_yang_validators = [self.validate_bgp_peer_group,
118+
self.validate_lanes]
119+
116120
try:
117121
tmp_config_db_as_json = copy.deepcopy(config_db_as_json)
118122

119123
sy.loadData(tmp_config_db_as_json)
120124

121125
sy.validate_data_tree()
122126

123-
# TODO: modularize custom validations better or move directly to sonic-yang module
124-
return self.validate_bgp_peer_group(config_db_as_json)
127+
for supplemental_yang_validator in supplemental_yang_validators:
128+
success, error = supplemental_yang_validator(config_db_as_json)
129+
if not success:
130+
return success, error
125131
except sonic_yang.SonicYangException as ex:
126132
return False, ex
127133

134+
return True, None
135+
136+
def validate_lanes(self, config_db):
137+
if "PORT" not in config_db:
138+
return True, None
139+
140+
ports = config_db["PORT"]
141+
142+
# Validate each lane separately, make sure it is not empty, and is a number
143+
port_to_lanes_map = {}
144+
for port in ports:
145+
attrs = ports[port]
146+
if "lanes" in attrs:
147+
lanes_str = attrs["lanes"]
148+
lanes_with_whitespaces = lanes_str.split(",")
149+
lanes = [lane.strip() for lane in lanes_with_whitespaces]
150+
for lane in lanes:
151+
if not lane:
152+
return False, f"PORT '{port}' has an empty lane"
153+
if not lane.isdigit():
154+
return False, f"PORT '{port}' has an invalid lane '{lane}'"
155+
port_to_lanes_map[port] = lanes
156+
157+
# Validate lanes are unique
158+
existing = {}
159+
for port in port_to_lanes_map:
160+
lanes = port_to_lanes_map[port]
161+
for lane in lanes:
162+
if lane in existing:
163+
return False, f"'{lane}' lane is used multiple times in PORT: {set([port, existing[lane]])}"
164+
existing[lane] = port
165+
return True, None
166+
128167
def validate_bgp_peer_group(self, config_db):
129168
if "BGP_PEER_RANGE" not in config_db:
130169
return True, None

generic_config_updater/patch_sorter.py

-25
Original file line numberDiff line numberDiff line change
@@ -565,30 +565,6 @@ def validate(self, move, diff):
565565
is_valid, error = self.config_wrapper.validate_config_db_config(simulated_config)
566566
return is_valid
567567

568-
# TODO: Add this validation to YANG models instead
569-
class UniqueLanesMoveValidator:
570-
"""
571-
A class to validate lanes and any port are unique between all ports.
572-
"""
573-
def validate(self, move, diff):
574-
simulated_config = move.apply(diff.current_config)
575-
576-
if "PORT" not in simulated_config:
577-
return True
578-
579-
ports = simulated_config["PORT"]
580-
existing = set()
581-
for port in ports:
582-
attrs = ports[port]
583-
if "lanes" in attrs:
584-
lanes_str = attrs["lanes"]
585-
lanes = lanes_str.split(", ")
586-
for lane in lanes:
587-
if lane in existing:
588-
return False
589-
existing.add(lane)
590-
return True
591-
592568
class CreateOnlyMoveValidator:
593569
"""
594570
A class to validate create-only fields are only created, but never modified/updated. In other words:
@@ -1507,7 +1483,6 @@ def create(self, algorithm=Algorithm.DFS):
15071483
move_validators = [DeleteWholeConfigMoveValidator(),
15081484
FullConfigMoveValidator(self.config_wrapper),
15091485
NoDependencyMoveValidator(self.path_addressing, self.config_wrapper),
1510-
UniqueLanesMoveValidator(),
15111486
CreateOnlyMoveValidator(self.path_addressing),
15121487
RequiredValueMoveValidator(self.path_addressing),
15131488
NoEmptyTableMoveValidator(self.path_addressing)]

tests/generic_config_updater/gu_common_test.py

+78
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,84 @@ def check_validate_bgp_peer_group(self, ip_range, other_ip_range=[], duplicated_
260260
self.assertFalse(actual)
261261
self.assertTrue(duplicated_ip in error)
262262

263+
def test_validate_lanes__no_port_table__success(self):
264+
config = {"ACL_TABLE": {}}
265+
self.validate_lanes(config)
266+
267+
def test_validate_lanes__empty_port_table__success(self):
268+
config = {"PORT": {}}
269+
self.validate_lanes(config)
270+
271+
def test_validate_lanes__empty_lane__failure(self):
272+
config = {"PORT": {"Ethernet0": {"lanes": "", "speed":"10000"}}}
273+
self.validate_lanes(config, 'has an empty lane')
274+
275+
def test_validate_lanes__whitespace_lane__failure(self):
276+
config = {"PORT": {"Ethernet0": {"lanes": " ", "speed":"10000"}}}
277+
self.validate_lanes(config, 'has an empty lane')
278+
279+
def test_validate_lanes__non_digits_lane__failure(self):
280+
config = {"PORT": {"Ethernet0": {"lanes": "10g", "speed":"10000"}}}
281+
self.validate_lanes(config, "has an invalid lane '10g'")
282+
283+
def test_validate_lanes__space_between_digits_lane__failure(self):
284+
config = {"PORT": {"Ethernet0": {"lanes": " 1 0 ", "speed":"10000"}}}
285+
self.validate_lanes(config, "has an invalid lane '1 0'")
286+
287+
def test_validate_lanes__single_valid_lane__success(self):
288+
config = {"PORT": {"Ethernet0": {"lanes": "66", "speed":"10000"}}}
289+
self.validate_lanes(config)
290+
291+
def test_validate_lanes__different_valid_lanes_single_port__success(self):
292+
config = {"PORT": {"Ethernet0": {"lanes": "66, 67, 68", "speed":"10000"}}}
293+
self.validate_lanes(config)
294+
295+
def test_validate_lanes__different_valid_and_invalid_empty_lanes_single_port__failure(self):
296+
config = {"PORT": {"Ethernet0": {"lanes": "66, , 68", "speed":"10000"}}}
297+
self.validate_lanes(config, 'has an empty lane')
298+
299+
def test_validate_lanes__different_valid_and_invalid_non_digit_lanes_single_port__failure(self):
300+
config = {"PORT": {"Ethernet0": {"lanes": "66, 67, 10g", "speed":"10000"}}}
301+
self.validate_lanes(config, "has an invalid lane '10g'")
302+
303+
def test_validate_lanes__different_valid_lanes_multi_ports__success(self):
304+
config = {"PORT": {
305+
"Ethernet0": {"lanes": " 64 , 65 \t", "speed":"10000"},
306+
"Ethernet1": {"lanes": " 66 , 67 \r\t\n, 68 ", "speed":"10000"},
307+
}}
308+
self.validate_lanes(config)
309+
310+
def test_validate_lanes__same_valid_lanes_single_port__failure(self):
311+
config = {"PORT": {"Ethernet0": {"lanes": "65 \r\t\n, 65", "speed":"10000"}}}
312+
self.validate_lanes(config, '65')
313+
314+
def test_validate_lanes__same_valid_lanes_multi_ports__failure(self):
315+
config = {"PORT": {
316+
"Ethernet0": {"lanes": "64, 65, 67", "speed":"10000"},
317+
"Ethernet1": {"lanes": "66, 67, 68", "speed":"10000"},
318+
}}
319+
self.validate_lanes(config, '67')
320+
321+
def test_validate_lanes__same_valid_lanes_multi_ports_no_spaces__failure(self):
322+
config = {"PORT": {
323+
"Ethernet0": {"lanes": "64,65,67", "speed":"10000"},
324+
"Ethernet1": {"lanes": "66,67,68", "speed":"10000"},
325+
}}
326+
self.validate_lanes(config, '67')
327+
328+
def validate_lanes(self, config_db, expected_error=None):
329+
# Arrange
330+
config_wrapper = gu_common.ConfigWrapper()
331+
expected = expected_error is None # if expected_error is None, then the input is valid
332+
333+
# Act
334+
actual, error = config_wrapper.validate_lanes(config_db)
335+
336+
# Assert
337+
self.assertEqual(expected, actual)
338+
if expected_error:
339+
self.assertTrue(expected_error in error)
340+
263341
def test_crop_tables_without_yang__returns_cropped_config_db_as_json(self):
264342
# Arrange
265343
config_wrapper = gu_common.ConfigWrapper()

tests/generic_config_updater/patch_sorter_test.py

-44
Original file line numberDiff line numberDiff line change
@@ -868,49 +868,6 @@ def verify(self, operation_type, path, expected):
868868
# Assert
869869
self.assertEqual(expected, actual)
870870

871-
class TestUniqueLanesMoveValidator(unittest.TestCase):
872-
def setUp(self):
873-
self.validator = ps.UniqueLanesMoveValidator()
874-
875-
def test_validate__no_port_table__success(self):
876-
config = {"ACL_TABLE": {}}
877-
self.validate_target_config(config)
878-
879-
def test_validate__empty_port_table__success(self):
880-
config = {"PORT": {}}
881-
self.validate_target_config(config)
882-
883-
def test_validate__single_lane__success(self):
884-
config = {"PORT": {"Ethernet0": {"lanes": "66", "speed":"10000"}}}
885-
self.validate_target_config(config)
886-
887-
def test_validate__different_lanes_single_port___success(self):
888-
config = {"PORT": {"Ethernet0": {"lanes": "66, 67, 68", "speed":"10000"}}}
889-
self.validate_target_config(config)
890-
891-
def test_validate__different_lanes_multi_ports___success(self):
892-
config = {"PORT": {
893-
"Ethernet0": {"lanes": "64, 65", "speed":"10000"},
894-
"Ethernet1": {"lanes": "66, 67, 68", "speed":"10000"},
895-
}}
896-
self.validate_target_config(config)
897-
898-
def test_validate__same_lanes_single_port___success(self):
899-
config = {"PORT": {"Ethernet0": {"lanes": "65, 65", "speed":"10000"}}}
900-
self.validate_target_config(config, False)
901-
902-
def validate_target_config(self, target_config, expected=True):
903-
# Arrange
904-
current_config = {}
905-
diff = ps.Diff(current_config, target_config)
906-
move = ps.JsonMove(diff, OperationType.REPLACE, [], [])
907-
908-
# Act
909-
actual = self.validator.validate(move, diff)
910-
911-
# Assert
912-
self.assertEqual(expected, actual)
913-
914871
class TestFullConfigMoveValidator(unittest.TestCase):
915872
def setUp(self):
916873
self.any_current_config = Mock()
@@ -3038,7 +2995,6 @@ def verify(self, algo, algo_class):
30382995
expected_validator = [ps.DeleteWholeConfigMoveValidator,
30392996
ps.FullConfigMoveValidator,
30402997
ps.NoDependencyMoveValidator,
3041-
ps.UniqueLanesMoveValidator,
30422998
ps.CreateOnlyMoveValidator,
30432999
ps.RequiredValueMoveValidator,
30443000
ps.NoEmptyTableMoveValidator]

0 commit comments

Comments
 (0)