@@ -544,6 +544,125 @@ def _get_default_value_from_settings(self, parent_path, field_name):
544
544
545
545
return None
546
546
547
+ class CreateOnlyFilter :
548
+ """
549
+ A filtering class for create-only fields.
550
+ """
551
+ def __init__ (self , path_addressing ):
552
+ # TODO: create-only fields are hard-coded for now, it should be moved to YANG model
553
+ self .path_addressing = path_addressing
554
+ self .patterns = [
555
+ ["PORT" , "*" , "lanes" ],
556
+ ["LOOPBACK_INTERFACE" , "*" , "vrf_name" ],
557
+ ["BGP_NEIGHBOR" , "*" , "holdtime" ],
558
+ ["BGP_NEIGHBOR" , "*" , "keepalive" ],
559
+ ["BGP_NEIGHBOR" , "*" , "name" ],
560
+ ["BGP_NEIGHBOR" , "*" , "asn" ],
561
+ ["BGP_NEIGHBOR" , "*" , "local_addr" ],
562
+ ["BGP_NEIGHBOR" , "*" , "nhopself" ],
563
+ ["BGP_NEIGHBOR" , "*" , "rrclient" ],
564
+ ["BGP_PEER_RANGE" , "*" , "*" ],
565
+ ["BGP_MONITORS" , "*" , "holdtime" ],
566
+ ["BGP_MONITORS" , "*" , "keepalive" ],
567
+ ["BGP_MONITORS" , "*" , "name" ],
568
+ ["BGP_MONITORS" , "*" , "asn" ],
569
+ ["BGP_MONITORS" , "*" , "local_addr" ],
570
+ ["BGP_MONITORS" , "*" , "nhopself" ],
571
+ ["BGP_MONITORS" , "*" , "rrclient" ],
572
+ ["MIRROR_SESSION" , "*" , "*" ],
573
+ ]
574
+
575
+ def get_filter (self ):
576
+ return JsonPointerFilter (self .patterns ,
577
+ self .path_addressing )
578
+
579
+ class RemoveCreateOnlyDependencyMoveValidator :
580
+ """
581
+ A class to validate all dependencies of create-only fields have been removed before
582
+ modifying the create-only fields.
583
+ """
584
+ def __init__ (self , path_addressing ):
585
+ self .path_addressing = path_addressing
586
+ self .create_only_filter = CreateOnlyFilter (path_addressing ).get_filter ()
587
+
588
+ def validate (self , move , diff ):
589
+ current_config = diff .current_config
590
+ target_config = diff .target_config # Final config after applying whole patch
591
+
592
+ processed_tables = set ()
593
+ for path in self .create_only_filter .get_paths (current_config ):
594
+ tokens = self .path_addressing .get_path_tokens (path )
595
+ table_to_check = tokens [0 ]
596
+
597
+ if table_to_check in processed_tables :
598
+ continue
599
+ else :
600
+ processed_tables .add (table_to_check )
601
+
602
+ if table_to_check not in current_config :
603
+ continue
604
+
605
+ current_members = current_config [table_to_check ]
606
+ if not current_members :
607
+ continue
608
+
609
+ if table_to_check not in target_config :
610
+ continue
611
+
612
+ target_members = target_config [table_to_check ]
613
+ if not target_members :
614
+ continue
615
+
616
+ simulated_config = move .apply (current_config ) # Config after applying just this move
617
+
618
+ for member_name in current_members :
619
+ if member_name not in target_members :
620
+ continue
621
+
622
+ if not self ._validate_member (tokens , member_name ,
623
+ current_config , target_config , simulated_config ):
624
+ return False
625
+
626
+ return True
627
+
628
+ def _validate_member (self , tokens , member_name , current_config , target_config , simulated_config ):
629
+ table_to_check , create_only_field = tokens [0 ], tokens [- 1 ]
630
+
631
+ current_field = self ._get_create_only_field (
632
+ current_config , table_to_check , member_name , create_only_field )
633
+ target_field = self ._get_create_only_field (
634
+ target_config , table_to_check , member_name , create_only_field )
635
+
636
+ if current_field == target_field :
637
+ return True
638
+
639
+ simulated_member = self .path_addressing .get_from_path (
640
+ simulated_config , f"/{ table_to_check } /{ member_name } " )
641
+
642
+ if simulated_member is None :
643
+ return True
644
+
645
+ if table_to_check == "PORT" :
646
+ current_admin_status = self .path_addressing .get_from_path (
647
+ current_config , f"/{ table_to_check } /{ member_name } /admin_status"
648
+ )
649
+ simulated_admin_status = self .path_addressing .get_from_path (
650
+ simulated_config , f"/{ table_to_check } /{ member_name } /admin_status"
651
+ )
652
+ if current_admin_status != simulated_admin_status and current_admin_status != "up" :
653
+ return False
654
+
655
+ member_path = f"/{ table_to_check } /{ member_name } "
656
+ for ref_path in self .path_addressing .find_ref_paths (member_path , simulated_config ):
657
+ if not self .path_addressing .has_path (current_config , ref_path ):
658
+ return False
659
+
660
+ return True
661
+
662
+ def _get_create_only_field (self , config , table_to_check ,
663
+ member_name , create_only_field ):
664
+ return config [table_to_check ][member_name ].get (create_only_field , None )
665
+
547
666
class DeleteWholeConfigMoveValidator :
548
667
"""
549
668
A class to validate not deleting whole config as it is not supported by JsonPatch lib.
@@ -576,27 +695,7 @@ def __init__(self, path_addressing):
576
695
self .path_addressing = path_addressing
577
696
578
697
# TODO: create-only fields are hard-coded for now, it should be moved to YANG models
579
- self .create_only_filter = JsonPointerFilter ([
580
- ["PORT" , "*" , "lanes" ],
581
- ["LOOPBACK_INTERFACE" , "*" , "vrf_name" ],
582
- ["BGP_NEIGHBOR" , "*" , "holdtime" ],
583
- ["BGP_NEIGHBOR" , "*" , "keepalive" ],
584
- ["BGP_NEIGHBOR" , "*" , "name" ],
585
- ["BGP_NEIGHBOR" , "*" , "asn" ],
586
- ["BGP_NEIGHBOR" , "*" , "local_addr" ],
587
- ["BGP_NEIGHBOR" , "*" , "nhopself" ],
588
- ["BGP_NEIGHBOR" , "*" , "rrclient" ],
589
- ["BGP_PEER_RANGE" , "*" , "*" ],
590
- ["BGP_MONITORS" , "*" , "holdtime" ],
591
- ["BGP_MONITORS" , "*" , "keepalive" ],
592
- ["BGP_MONITORS" , "*" , "name" ],
593
- ["BGP_MONITORS" , "*" , "asn" ],
594
- ["BGP_MONITORS" , "*" , "local_addr" ],
595
- ["BGP_MONITORS" , "*" , "nhopself" ],
596
- ["BGP_MONITORS" , "*" , "rrclient" ],
597
- ["MIRROR_SESSION" , "*" , "*" ],
598
- ],
599
- path_addressing )
698
+ self .create_only_filter = CreateOnlyFilter (path_addressing ).get_filter ()
600
699
601
700
def validate (self , move , diff ):
602
701
simulated_config = move .apply (diff .current_config )
@@ -921,6 +1020,8 @@ def validate(self, move, diff):
921
1020
for required_path , required_value in data [path ]:
922
1021
current_value = self .identifier .get_value_or_default (current_config , required_path )
923
1022
simulated_value = self .identifier .get_value_or_default (simulated_config , required_path )
1023
+ if simulated_value is None : # Simulated config does not have this value at all.
1024
+ continue
924
1025
if current_value != simulated_value and simulated_value != required_value :
925
1026
return False
926
1027
@@ -1000,6 +1101,65 @@ def generate(self, diff):
1000
1101
for move in single_run_generator .generate ():
1001
1102
yield move
1002
1103
1104
+ class RemoveCreateOnlyDependencyMoveGenerator :
1105
+ """
1106
+ A class to generate the create-only fields' dependency removing moves
1107
+ """
1108
+ def __init__ (self , path_addressing ):
1109
+ self .path_addressing = path_addressing
1110
+ self .create_only_filter = CreateOnlyFilter (path_addressing ).get_filter ()
1111
+
1112
+ def generate (self , diff ):
1113
+ current_config = diff .current_config
1114
+ target_config = diff .target_config # Final config after applying whole patch
1115
+
1116
+ processed_tables = set ()
1117
+ for path in self .create_only_filter .get_paths (current_config ):
1118
+ tokens = self .path_addressing .get_path_tokens (path )
1119
+ table_to_check , create_only_field = tokens [0 ], tokens [- 1 ]
1120
+
1121
+ if table_to_check in processed_tables :
1122
+ continue
1123
+ else :
1124
+ processed_tables .add (table_to_check )
1125
+
1126
+ if table_to_check not in current_config :
1127
+ continue
1128
+
1129
+ current_members = current_config [table_to_check ]
1130
+ if not current_members :
1131
+ continue
1132
+
1133
+ if table_to_check not in target_config :
1134
+ continue
1135
+
1136
+ target_members = target_config [table_to_check ]
1137
+ if not target_members :
1138
+ continue
1139
+
1140
+ for member_name in current_members :
1141
+ if member_name not in target_members :
1142
+ continue
1143
+
1144
+ current_field = self ._get_create_only_field (
1145
+ current_config , table_to_check , member_name , create_only_field )
1146
+ target_field = self ._get_create_only_field (
1147
+ target_config , table_to_check , member_name , create_only_field )
1148
+
1149
+ if current_field == target_field :
1150
+ continue
1151
+
1152
+ member_path = f"/{ table_to_check } /{ member_name } "
1153
+
1154
+ for ref_path in self .path_addressing .find_ref_paths (member_path , current_config ):
1155
+ yield JsonMove (diff , OperationType .REMOVE ,
1156
+ self .path_addressing .get_path_tokens (ref_path ))
1157
+
1158
+ def _get_create_only_field (self , config , table_to_check ,
1159
+ member_name , create_only_field ):
1160
+ return config [table_to_check ][member_name ].get (create_only_field , None )
1161
+
1162
+
1003
1163
class SingleRunLowLevelMoveGenerator :
1004
1164
"""
1005
1165
A class that can only run once to assist LowLevelMoveGenerator with generating the moves.
@@ -1271,6 +1431,8 @@ def extend(self, move, diff):
1271
1431
for required_path , required_value in data [path ]:
1272
1432
current_value = self .identifier .get_value_or_default (current_config , required_path )
1273
1433
simulated_value = self .identifier .get_value_or_default (simulated_config , required_path )
1434
+ if simulated_value is None : # Simulated config does not have this value at all.
1435
+ continue
1274
1436
if current_value != simulated_value and simulated_value != required_value :
1275
1437
flip_path_value_tuples .add ((required_path , required_value ))
1276
1438
@@ -1473,7 +1635,8 @@ def __init__(self, operation_wrapper, config_wrapper, path_addressing):
1473
1635
self .path_addressing = path_addressing
1474
1636
1475
1637
def create (self , algorithm = Algorithm .DFS ):
1476
- move_generators = [LowLevelMoveGenerator (self .path_addressing )]
1638
+ move_generators = [RemoveCreateOnlyDependencyMoveGenerator (self .path_addressing ),
1639
+ LowLevelMoveGenerator (self .path_addressing )]
1477
1640
# TODO: Enable TableLevelMoveGenerator once it is confirmed whole table can be updated at the same time
1478
1641
move_non_extendable_generators = [KeyLevelMoveGenerator ()]
1479
1642
move_extenders = [RequiredValueMoveExtender (self .path_addressing , self .operation_wrapper ),
@@ -1485,6 +1648,7 @@ def create(self, algorithm=Algorithm.DFS):
1485
1648
NoDependencyMoveValidator (self .path_addressing , self .config_wrapper ),
1486
1649
CreateOnlyMoveValidator (self .path_addressing ),
1487
1650
RequiredValueMoveValidator (self .path_addressing ),
1651
+ RemoveCreateOnlyDependencyMoveValidator (self .path_addressing ),
1488
1652
NoEmptyTableMoveValidator (self .path_addressing )]
1489
1653
1490
1654
move_wrapper = MoveWrapper (move_generators , move_non_extendable_generators , move_extenders , move_validators )
0 commit comments