@@ -372,40 +372,58 @@ def validate(self, move, diff):
372
372
373
373
class CreateOnlyMoveValidator :
374
374
"""
375
- A class to validate create-only fields are only added/removed but never replaced.
376
- Parents of create-only fields are also only added/removed but never replaced when they contain
377
- a modified create-only field.
375
+ A class to validate create-only fields are only created, but never modified/updated. In other words:
376
+ - Field cannot be replaced.
377
+ - Field cannot be added, only if the parent is added.
378
+ - Field cannot be deleted, only if the parent is deleted.
378
379
"""
379
380
def __init__ (self , path_addressing ):
380
381
self .path_addressing = path_addressing
381
382
382
- def validate (self , move , diff ):
383
- if move .op_type != OperationType .REPLACE :
384
- return True
383
+ # TODO: create-only fields are hard-coded for now, it should be moved to YANG models
384
+ # Each pattern consist of a list of tokens. Token matching starts from the root level of the config.
385
+ # Each token is either a specific key or '*' to match all keys.
386
+ self .create_only_patterns = [
387
+ ["PORT" , "*" , "lanes" ],
388
+ ["LOOPBACK_INTERFACE" , "*" , "vrf_name" ],
389
+ ]
385
390
386
- # The 'create-only' field needs to be common between current and simulated anyway but different.
387
- # This means it is enough to just get the paths from current_config, paths that are not common can be ignored.
388
- paths = self ._get_create_only_paths (diff .current_config )
391
+ def validate (self , move , diff ):
389
392
simulated_config = move .apply (diff .current_config )
393
+ paths = set (list (self ._get_create_only_paths (diff .current_config )) + list (self ._get_create_only_paths (simulated_config )))
390
394
391
395
for path in paths :
392
396
tokens = self .path_addressing .get_path_tokens (path )
393
397
if self ._value_exist_but_different (tokens , diff .current_config , simulated_config ):
394
398
return False
399
+ if self ._value_added_but_parent_exist (tokens , diff .current_config , simulated_config ):
400
+ return False
401
+ if self ._value_removed_but_parent_remain (tokens , diff .current_config , simulated_config ):
402
+ return False
395
403
396
404
return True
397
405
398
- # TODO: create-only fields are hard-coded for now, it should be moved to YANG models
399
406
def _get_create_only_paths (self , config ):
400
- if "PORT" not in config :
407
+ for pattern in self .create_only_patterns :
408
+ for create_only_path in self ._get_create_only_path_recursive (config , pattern , [], 0 ):
409
+ yield create_only_path
410
+
411
+ def _get_create_only_path_recursive (self , config , pattern_tokens , matching_tokens , idx ):
412
+ if idx == len (pattern_tokens ):
413
+ yield '/' + '/' .join (matching_tokens )
401
414
return
402
415
403
- ports = config ["PORT" ]
416
+ matching_keys = []
417
+ if pattern_tokens [idx ] == "*" :
418
+ matching_keys = config .keys ()
419
+ elif pattern_tokens [idx ] in config :
420
+ matching_keys = [pattern_tokens [idx ]]
404
421
405
- for port in ports :
406
- attrs = ports [port ]
407
- if "lanes" in attrs :
408
- yield f"/PORT/{ port } /lanes"
422
+ for key in matching_keys :
423
+ matching_tokens .append (key )
424
+ for create_only_path in self ._get_create_only_path_recursive (config [key ], pattern_tokens , matching_tokens , idx + 1 ):
425
+ yield create_only_path
426
+ matching_tokens .pop ()
409
427
410
428
def _value_exist_but_different (self , tokens , current_config_ptr , simulated_config_ptr ):
411
429
for token in tokens :
@@ -422,6 +440,46 @@ def _value_exist_but_different(self, tokens, current_config_ptr, simulated_confi
422
440
423
441
return current_config_ptr != simulated_config_ptr
424
442
443
+ def _value_added_but_parent_exist (self , tokens , current_config_ptr , simulated_config_ptr ):
444
+ # if value is not added, return false
445
+ if not self ._exist_only_in_first (tokens , simulated_config_ptr , current_config_ptr ):
446
+ return False
447
+
448
+ # if parent is added, return false
449
+ if self ._exist_only_in_first (tokens [:- 1 ], simulated_config_ptr , current_config_ptr ):
450
+ return False
451
+
452
+ # otherwise parent exist and value is added
453
+ return True
454
+
455
+ def _value_removed_but_parent_remain (self , tokens , current_config_ptr , simulated_config_ptr ):
456
+ # if value is not removed, return false
457
+ if not self ._exist_only_in_first (tokens , current_config_ptr , simulated_config_ptr ):
458
+ return False
459
+
460
+ # if parent is removed, return false
461
+ if self ._exist_only_in_first (tokens [:- 1 ], current_config_ptr , simulated_config_ptr ):
462
+ return False
463
+
464
+ # otherwise parent remained and value is removed
465
+ return True
466
+
467
+ def _exist_only_in_first (self , tokens , first_config_ptr , second_config_ptr ):
468
+ for token in tokens :
469
+ mod_token = int (token ) if isinstance (first_config_ptr , list ) else token
470
+
471
+ if mod_token not in second_config_ptr :
472
+ return True
473
+
474
+ if mod_token not in first_config_ptr :
475
+ return False
476
+
477
+ first_config_ptr = first_config_ptr [mod_token ]
478
+ second_config_ptr = second_config_ptr [mod_token ]
479
+
480
+ # tokens exist in both
481
+ return False
482
+
425
483
class NoDependencyMoveValidator :
426
484
"""
427
485
A class to validate that the modified configs do not have dependency on each other. This should prevent
0 commit comments