Skip to content

Commit 818dcbf

Browse files
authored
[GCU] Implementing DryRun by printing patch-sorter steps/imitating config_db (sonic-net#1973)
#### What I did Implementing `dry-run` option flag. - Supports only printing the steps generated from patch-sorting - TODO in a future PR: Print the `SET` commands sent to `config_db` - TODO in a future PR: Print the service validation commands #### How I did it By implementing the DryRunConfigWrapper. - Whenever a dry-run is issued, the CLI output will start with `** DRY RUN EXECUTION **` - At each step we simulate `config_db`, we print a log msg starting with `** DryRun: Would ` #### How to verify it #### 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) **Apply patch** ``` admin@vlab-01:~$ sudo config apply-patch remove-acl-table.json-patch -d -i /BGP_NEIGHBOR -i /FEATURE -i /QUEUE -i /VLAN/Vlan1000/members -i /DEVICE_METADATA -i /FLEX_COUNTER_TABLE -i /SCHEDULER ** DRY RUN EXECUTION ** Patch Applier: Patch application starting. Patch Applier: Patch: [{"op": "remove", "path": "/ACL_TABLE/DATAACL"}] Patch Applier: Getting current config db. Patch Applier: Simulating the target full config after applying the patch. Patch Applier: Validating target config does not have empty tables, since they do not show up in ConfigDb. Patch Applier: Sorting patch updates. Note: Below table(s) have no YANG models: BGP_PEER_RANGE, CABLE_LENGTH, CONSOLE_SWITCH, DEVICE_NEIGHBOR_METADATA, DHCP_SERVER, KDUMP, RESTAPI, SNMP, SNMP_COMMUNITY, SYSLOG_SERVER, TELEMETRY, Note: Below table(s) have no YANG models: BGP_PEER_RANGE, CABLE_LENGTH, CONSOLE_SWITCH, DEVICE_NEIGHBOR_METADATA, DHCP_SERVER, KDUMP, RESTAPI, SNMP, SNMP_COMMUNITY, SYSLOG_SERVER, TELEMETRY, Note: Below table(s) have no YANG models: BGP_PEER_RANGE, CABLE_LENGTH, CONSOLE_SWITCH, DEVICE_NEIGHBOR_METADATA, DHCP_SERVER, KDUMP, RESTAPI, SNMP, SNMP_COMMUNITY, SYSLOG_SERVER, TELEMETRY, libyang[0]: Invalid JSON data (unexpected value). (path: /sonic-acl:sonic-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='DATAACL']/ports) sonic_yang(3):Data Loading Failed:Invalid JSON data (unexpected value). libyang[0]: Invalid JSON data (unexpected value). (path: /sonic-acl:sonic-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='DATAACL']/ports) sonic_yang(3):Data Loading Failed:Invalid JSON data (unexpected value). libyang[0]: Missing required element "type" in "ACL_TABLE_LIST". (path: /sonic-acl:sonic-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='DATAACL']) sonic_yang(3):Data Loading Failed:Missing required element "type" in "ACL_TABLE_LIST". libyang[0]: Missing required element "type" in "ACL_TABLE_LIST". (path: /sonic-acl:sonic-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='DATAACL']) sonic_yang(3):Data Loading Failed:Missing required element "type" in "ACL_TABLE_LIST". Patch Applier: The patch was sorted into 7 changes: Patch Applier: * [{"op": "remove", "path": "/ACL_TABLE/DATAACL/policy_desc"}] Patch Applier: * [{"op": "remove", "path": "/ACL_TABLE/DATAACL/ports/0"}] Patch Applier: * [{"op": "remove", "path": "/ACL_TABLE/DATAACL/ports/0"}] Patch Applier: * [{"op": "remove", "path": "/ACL_TABLE/DATAACL/ports/0"}] Patch Applier: * [{"op": "remove", "path": "/ACL_TABLE/DATAACL/stage"}] Patch Applier: * [{"op": "remove", "path": "/ACL_TABLE/DATAACL/ports"}] Patch Applier: * [{"op": "remove", "path": "/ACL_TABLE/DATAACL"}] Patch Applier: Applying 7 changes in order: Patch Applier: * [{"op": "remove", "path": "/ACL_TABLE/DATAACL/policy_desc"}] ** DryRun: Would apply [{"op": "remove", "path": "/ACL_TABLE/DATAACL/policy_desc"}] Patch Applier: * [{"op": "remove", "path": "/ACL_TABLE/DATAACL/ports/0"}] ** DryRun: Would apply [{"op": "remove", "path": "/ACL_TABLE/DATAACL/ports/0"}] Patch Applier: * [{"op": "remove", "path": "/ACL_TABLE/DATAACL/ports/0"}] ** DryRun: Would apply [{"op": "remove", "path": "/ACL_TABLE/DATAACL/ports/0"}] Patch Applier: * [{"op": "remove", "path": "/ACL_TABLE/DATAACL/ports/0"}] ** DryRun: Would apply [{"op": "remove", "path": "/ACL_TABLE/DATAACL/ports/0"}] Patch Applier: * [{"op": "remove", "path": "/ACL_TABLE/DATAACL/stage"}] ** DryRun: Would apply [{"op": "remove", "path": "/ACL_TABLE/DATAACL/stage"}] Patch Applier: * [{"op": "remove", "path": "/ACL_TABLE/DATAACL/ports"}] ** DryRun: Would apply [{"op": "remove", "path": "/ACL_TABLE/DATAACL/ports"}] Patch Applier: * [{"op": "remove", "path": "/ACL_TABLE/DATAACL"}] ** DryRun: Would apply [{"op": "remove", "path": "/ACL_TABLE/DATAACL"}] Patch Applier: Verifying patch updates are reflected on ConfigDB. Patch Applier: Patch application completed. Patch applied successfully. admin@vlab-01:~$ ``` **Config rollback** ``` admin@vlab-01:~$ sudo config rollback cp1 -d -i /BGP_NEIGHBOR -i /FEATURE -i /QUEUE -i /VLAN/Vlan1000/members -i /DEVICE_METADATA -i /FLEX_COUNTER_TABLE -i /SCHEDULER ** DRY RUN EXECUTION ** Config Rollbacker: Config rollbacking starting. Config Rollbacker: Checkpoint name: cp1. Config Rollbacker: Verifying 'cp1' exists. Config Rollbacker: Loading checkpoint into memory. Config Rollbacker: Replacing config using 'Config Replacer'. Config Replacer: Config replacement starting. Config Replacer: Target config length: 49881. Config Replacer: Getting current config db. Config Replacer: Generating patch between target config and current config db. Config Replacer: Applying patch using 'Patch Applier'. Patch Applier: Patch application starting. Patch Applier: Patch: [{"op": "add", "path": "/ACL_TABLE/DATAACL", "value": {"policy_desc": "DATAACL", "ports": ["PortChannel0001", "PortChannel0002", "PortChannel0003", "PortChannel0004"], "stage": "ingress", "type": "L3"}}] Patch Applier: Getting current config db. Patch Applier: Simulating the target full config after applying the patch. Patch Applier: Validating target config does not have empty tables, since they do not show up in ConfigDb. Patch Applier: Sorting patch updates. Note: Below table(s) have no YANG models: BGP_PEER_RANGE, CABLE_LENGTH, CONSOLE_SWITCH, DEVICE_NEIGHBOR_METADATA, DHCP_SERVER, KDUMP, RESTAPI, SNMP, SNMP_COMMUNITY, SYSLOG_SERVER, TELEMETRY, Note: Below table(s) have no YANG models: BGP_PEER_RANGE, CABLE_LENGTH, CONSOLE_SWITCH, DEVICE_NEIGHBOR_METADATA, DHCP_SERVER, KDUMP, RESTAPI, SNMP, SNMP_COMMUNITY, SYSLOG_SERVER, TELEMETRY, Note: Below table(s) have no YANG models: BGP_PEER_RANGE, CABLE_LENGTH, CONSOLE_SWITCH, DEVICE_NEIGHBOR_METADATA, DHCP_SERVER, KDUMP, RESTAPI, SNMP, SNMP_COMMUNITY, SYSLOG_SERVER, TELEMETRY, libyang[0]: Missing required element "type" in "ACL_TABLE_LIST". (path: /sonic-acl:sonic-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='DATAACL']) sonic_yang(3):Data Loading Failed:Missing required element "type" in "ACL_TABLE_LIST". libyang[0]: Missing required element "type" in "ACL_TABLE_LIST". (path: /sonic-acl:sonic-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='DATAACL']) sonic_yang(3):Data Loading Failed:Missing required element "type" in "ACL_TABLE_LIST". libyang[0]: Missing required element "type" in "ACL_TABLE_LIST". (path: /sonic-acl:sonic-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='DATAACL']) sonic_yang(3):Data Loading Failed:Missing required element "type" in "ACL_TABLE_LIST". libyang[0]: Missing required element "type" in "ACL_TABLE_LIST". (path: /sonic-acl:sonic-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='DATAACL']) sonic_yang(3):Data Loading Failed:Missing required element "type" in "ACL_TABLE_LIST". libyang[0]: Missing required element "type" in "ACL_TABLE_LIST". (path: /sonic-acl:sonic-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='DATAACL']) sonic_yang(3):Data Loading Failed:Missing required element "type" in "ACL_TABLE_LIST". libyang[0]: Missing required element "type" in "ACL_TABLE_LIST". (path: /sonic-acl:sonic-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='DATAACL']) sonic_yang(3):Data Loading Failed:Missing required element "type" in "ACL_TABLE_LIST". Patch Applier: The patch was sorted into 7 changes: Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL", "value": {"type": "L3"}}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/policy_desc", "value": "DATAACL"}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports", "value": ["PortChannel0001"]}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports/1", "value": "PortChannel0002"}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports/2", "value": "PortChannel0003"}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports/3", "value": "PortChannel0004"}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/stage", "value": "ingress"}] Patch Applier: Applying 7 changes in order: Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL", "value": {"type": "L3"}}] ** DryRun: Would apply [{"op": "add", "path": "/ACL_TABLE/DATAACL", "value": {"type": "L3"}}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/policy_desc", "value": "DATAACL"}] ** DryRun: Would apply [{"op": "add", "path": "/ACL_TABLE/DATAACL/policy_desc", "value": "DATAACL"}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports", "value": ["PortChannel0001"]}] ** DryRun: Would apply [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports", "value": ["PortChannel0001"]}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports/1", "value": "PortChannel0002"}] ** DryRun: Would apply [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports/1", "value": "PortChannel0002"}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports/2", "value": "PortChannel0003"}] ** DryRun: Would apply [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports/2", "value": "PortChannel0003"}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports/3", "value": "PortChannel0004"}] ** DryRun: Would apply [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports/3", "value": "PortChannel0004"}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/stage", "value": "ingress"}] ** DryRun: Would apply [{"op": "add", "path": "/ACL_TABLE/DATAACL/stage", "value": "ingress"}] Patch Applier: Verifying patch updates are reflected on ConfigDB. Patch Applier: Patch application completed. Config Replacer: Verifying config replacement is reflected on ConfigDB. Config Replacer: Config replacement completed. Config Rollbacker: Config rollbacking completed. Config rolled back successfully. admin@vlab-01:~$ ``` **Config replace** ``` admin@vlab-01:~$ sudo config replace ~/cur.json -d -i /BGP_NEIGHBOR -i /FEATURE -i /QUEUE -i /VLAN/Vlan1000/members -i /DEVICE_METADATA -i /FLEX_COUNTER_TABLE -i /SCHEDULER -d ** DRY RUN EXECUTION ** Config Replacer: Config replacement starting. Config Replacer: Target config length: 49881. Config Replacer: Getting current config db. Config Replacer: Generating patch between target config and current config db. Config Replacer: Applying patch using 'Patch Applier'. Patch Applier: Patch application starting. Patch Applier: Patch: [{"op": "add", "path": "/ACL_TABLE/DATAACL", "value": {"policy_desc": "DATAACL", "ports": ["PortChannel0001", "PortChannel0002", "PortChannel0003", "PortChannel0004"], "stage": "ingress", "type": "L3"}}] Patch Applier: Getting current config db. Patch Applier: Simulating the target full config after applying the patch. Patch Applier: Validating target config does not have empty tables, since they do not show up in ConfigDb. Patch Applier: Sorting patch updates. Note: Below table(s) have no YANG models: BGP_PEER_RANGE, CABLE_LENGTH, CONSOLE_SWITCH, DEVICE_NEIGHBOR_METADATA, DHCP_SERVER, KDUMP, RESTAPI, SNMP, SNMP_COMMUNITY, SYSLOG_SERVER, TELEMETRY, Note: Below table(s) have no YANG models: BGP_PEER_RANGE, CABLE_LENGTH, CONSOLE_SWITCH, DEVICE_NEIGHBOR_METADATA, DHCP_SERVER, KDUMP, RESTAPI, SNMP, SNMP_COMMUNITY, SYSLOG_SERVER, TELEMETRY, Note: Below table(s) have no YANG models: BGP_PEER_RANGE, CABLE_LENGTH, CONSOLE_SWITCH, DEVICE_NEIGHBOR_METADATA, DHCP_SERVER, KDUMP, RESTAPI, SNMP, SNMP_COMMUNITY, SYSLOG_SERVER, TELEMETRY, libyang[0]: Missing required element "type" in "ACL_TABLE_LIST". (path: /sonic-acl:sonic-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='DATAACL']) sonic_yang(3):Data Loading Failed:Missing required element "type" in "ACL_TABLE_LIST". libyang[0]: Missing required element "type" in "ACL_TABLE_LIST". (path: /sonic-acl:sonic-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='DATAACL']) sonic_yang(3):Data Loading Failed:Missing required element "type" in "ACL_TABLE_LIST". libyang[0]: Missing required element "type" in "ACL_TABLE_LIST". (path: /sonic-acl:sonic-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='DATAACL']) sonic_yang(3):Data Loading Failed:Missing required element "type" in "ACL_TABLE_LIST". libyang[0]: Missing required element "type" in "ACL_TABLE_LIST". (path: /sonic-acl:sonic-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='DATAACL']) sonic_yang(3):Data Loading Failed:Missing required element "type" in "ACL_TABLE_LIST". libyang[0]: Missing required element "type" in "ACL_TABLE_LIST". (path: /sonic-acl:sonic-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='DATAACL']) sonic_yang(3):Data Loading Failed:Missing required element "type" in "ACL_TABLE_LIST". libyang[0]: Missing required element "type" in "ACL_TABLE_LIST". (path: /sonic-acl:sonic-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='DATAACL']) sonic_yang(3):Data Loading Failed:Missing required element "type" in "ACL_TABLE_LIST". Patch Applier: The patch was sorted into 7 changes: Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL", "value": {"type": "L3"}}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/policy_desc", "value": "DATAACL"}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports", "value": ["PortChannel0001"]}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports/1", "value": "PortChannel0002"}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports/2", "value": "PortChannel0003"}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports/3", "value": "PortChannel0004"}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/stage", "value": "ingress"}] Patch Applier: Applying 7 changes in order: Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL", "value": {"type": "L3"}}] ** DryRun: Would apply [{"op": "add", "path": "/ACL_TABLE/DATAACL", "value": {"type": "L3"}}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/policy_desc", "value": "DATAACL"}] ** DryRun: Would apply [{"op": "add", "path": "/ACL_TABLE/DATAACL/policy_desc", "value": "DATAACL"}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports", "value": ["PortChannel0001"]}] ** DryRun: Would apply [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports", "value": ["PortChannel0001"]}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports/1", "value": "PortChannel0002"}] ** DryRun: Would apply [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports/1", "value": "PortChannel0002"}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports/2", "value": "PortChannel0003"}] ** DryRun: Would apply [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports/2", "value": "PortChannel0003"}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports/3", "value": "PortChannel0004"}] ** DryRun: Would apply [{"op": "add", "path": "/ACL_TABLE/DATAACL/ports/3", "value": "PortChannel0004"}] Patch Applier: * [{"op": "add", "path": "/ACL_TABLE/DATAACL/stage", "value": "ingress"}] ** DryRun: Would apply [{"op": "add", "path": "/ACL_TABLE/DATAACL/stage", "value": "ingress"}] Patch Applier: Verifying patch updates are reflected on ConfigDB. Patch Applier: Patch application completed. Config Replacer: Verifying config replacement is reflected on ConfigDB. Config Replacer: Config replacement completed. Config replaced successfully. admin@vlab-01:~$ ```
1 parent fe00bbf commit 818dcbf

File tree

7 files changed

+130
-8
lines changed

7 files changed

+130
-8
lines changed

config/main.py

+10
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,10 @@ def load(filename, yes):
11641164
log.log_info("'load' executing...")
11651165
clicommon.run_command(command, display_cmd=True)
11661166

1167+
def print_dry_run_message(dry_run):
1168+
if dry_run:
1169+
click.secho("** DRY RUN EXECUTION **", fg="yellow", underline=True)
1170+
11671171
@config.command('apply-patch')
11681172
@click.argument('patch-file-path', type=str, required=True)
11691173
@click.option('-f', '--format', type=click.Choice([e.name for e in ConfigFormat]),
@@ -1182,6 +1186,8 @@ def apply_patch(ctx, patch_file_path, format, dry_run, ignore_non_yang_tables, i
11821186
11831187
<patch-file-path>: Path to the patch file on the file-system."""
11841188
try:
1189+
print_dry_run_message(dry_run)
1190+
11851191
with open(patch_file_path, 'r') as fh:
11861192
text = fh.read()
11871193
patch_as_json = json.loads(text)
@@ -1214,6 +1220,8 @@ def replace(ctx, target_file_path, format, dry_run, ignore_non_yang_tables, igno
12141220
12151221
<target-file-path>: Path to the target file on the file-system."""
12161222
try:
1223+
print_dry_run_message(dry_run)
1224+
12171225
with open(target_file_path, 'r') as fh:
12181226
target_config_as_text = fh.read()
12191227
target_config = json.loads(target_config_as_text)
@@ -1241,6 +1249,8 @@ def rollback(ctx, checkpoint_name, dry_run, ignore_non_yang_tables, ignore_path,
12411249
12421250
<checkpoint-name>: The checkpoint name, use `config list-checkpoints` command to see available checkpoints."""
12431251
try:
1252+
print_dry_run_message(dry_run)
1253+
12441254
GenericUpdater().rollback(checkpoint_name, verbose, dry_run, ignore_non_yang_tables, ignore_path)
12451255

12461256
click.secho("Config rolled back successfully.", fg="cyan", underline=True)

generic_config_updater/change_applier.py

+10
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ def prune_empty_table(data):
5555
return data
5656

5757

58+
class DryRunChangeApplier:
59+
60+
def __init__(self, config_wrapper):
61+
self.config_wrapper = config_wrapper
62+
63+
64+
def apply(self, change):
65+
self.config_wrapper.apply_change_to_config_db(change)
66+
67+
5868
class ChangeApplier:
5969

6070
updater_conf = None

generic_config_updater/generic_updater.py

+22-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
DryRunConfigWrapper, PatchWrapper, genericUpdaterLogging
66
from .patch_sorter import StrictPatchSorter, NonStrictPatchSorter, ConfigSplitter, \
77
TablesWithoutYangConfigSplitter, IgnorePathsFromYangConfigSplitter
8-
from .change_applier import ChangeApplier
8+
from .change_applier import ChangeApplier, DryRunChangeApplier
99

1010
CHECKPOINTS_DIR = "/etc/sonic/checkpoints"
1111
CHECKPOINT_EXT = ".cp.json"
@@ -299,9 +299,13 @@ class GenericUpdateFactory:
299299
def create_patch_applier(self, config_format, verbose, dry_run, ignore_non_yang_tables, ignore_paths):
300300
self.init_verbose_logging(verbose)
301301
config_wrapper = self.get_config_wrapper(dry_run)
302+
change_applier = self.get_change_applier(dry_run, config_wrapper)
302303
patch_wrapper = PatchWrapper(config_wrapper)
303304
patch_sorter = self.get_patch_sorter(ignore_non_yang_tables, ignore_paths, config_wrapper, patch_wrapper)
304-
patch_applier = PatchApplier(config_wrapper=config_wrapper, patchsorter=patch_sorter, patch_wrapper=patch_wrapper)
305+
patch_applier = PatchApplier(config_wrapper=config_wrapper,
306+
patchsorter=patch_sorter,
307+
patch_wrapper=patch_wrapper,
308+
changeapplier=change_applier)
305309

306310
if config_format == ConfigFormat.CONFIGDB:
307311
pass
@@ -320,9 +324,13 @@ def create_config_replacer(self, config_format, verbose, dry_run, ignore_non_yan
320324
self.init_verbose_logging(verbose)
321325

322326
config_wrapper = self.get_config_wrapper(dry_run)
327+
change_applier = self.get_change_applier(dry_run, config_wrapper)
323328
patch_wrapper = PatchWrapper(config_wrapper)
324329
patch_sorter = self.get_patch_sorter(ignore_non_yang_tables, ignore_paths, config_wrapper, patch_wrapper)
325-
patch_applier = PatchApplier(config_wrapper=config_wrapper, patchsorter=patch_sorter, patch_wrapper=patch_wrapper)
330+
patch_applier = PatchApplier(config_wrapper=config_wrapper,
331+
patchsorter=patch_sorter,
332+
patch_wrapper=patch_wrapper,
333+
changeapplier=change_applier)
326334

327335
config_replacer = ConfigReplacer(patch_applier=patch_applier, config_wrapper=config_wrapper)
328336
if config_format == ConfigFormat.CONFIGDB:
@@ -342,9 +350,13 @@ def create_config_rollbacker(self, verbose, dry_run=False, ignore_non_yang_table
342350
self.init_verbose_logging(verbose)
343351

344352
config_wrapper = self.get_config_wrapper(dry_run)
353+
change_applier = self.get_change_applier(dry_run, config_wrapper)
345354
patch_wrapper = PatchWrapper(config_wrapper)
346355
patch_sorter = self.get_patch_sorter(ignore_non_yang_tables, ignore_paths, config_wrapper, patch_wrapper)
347-
patch_applier = PatchApplier(config_wrapper=config_wrapper, patchsorter=patch_sorter, patch_wrapper=patch_wrapper)
356+
patch_applier = PatchApplier(config_wrapper=config_wrapper,
357+
patchsorter=patch_sorter,
358+
patch_wrapper=patch_wrapper,
359+
changeapplier=change_applier)
348360

349361
config_replacer = ConfigReplacer(config_wrapper=config_wrapper, patch_applier=patch_applier)
350362
config_rollbacker = FileSystemConfigRollbacker(config_wrapper = config_wrapper, config_replacer = config_replacer)
@@ -363,6 +375,12 @@ def get_config_wrapper(self, dry_run):
363375
else:
364376
return ConfigWrapper()
365377

378+
def get_change_applier(self, dry_run, config_wrapper):
379+
if dry_run:
380+
return DryRunChangeApplier(config_wrapper)
381+
else:
382+
return ChangeApplier()
383+
366384
def get_patch_sorter(self, ignore_non_yang_tables, ignore_paths, config_wrapper, patch_wrapper):
367385
if not ignore_non_yang_tables and not ignore_paths:
368386
return StrictPatchSorter(config_wrapper, patch_wrapper)

generic_config_updater/gu_common.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,26 @@ def remove_empty_tables(self, config):
152152
return config_with_non_empty_tables
153153

154154
class DryRunConfigWrapper(ConfigWrapper):
155-
# TODO: implement DryRunConfigWrapper
156155
# This class will simulate all read/write operations to ConfigDB on a virtual storage unit.
157-
pass
156+
def __init__(self, initial_imitated_config_db = None):
157+
super().__init__()
158+
self.logger = genericUpdaterLogging.get_logger(title="** DryRun", print_all_to_console=True)
159+
self.imitated_config_db = copy.deepcopy(initial_imitated_config_db)
160+
161+
def apply_change_to_config_db(self, change):
162+
self._init_imitated_config_db_if_none()
163+
self.logger.log_notice(f"Would apply {change}")
164+
self.imitated_config_db = change.apply(self.imitated_config_db)
165+
166+
def get_config_db_as_json(self):
167+
self._init_imitated_config_db_if_none()
168+
return self.imitated_config_db
169+
170+
def _init_imitated_config_db_if_none(self):
171+
# if there is no initial imitated config_db and it is the first time calling this method
172+
if self.imitated_config_db is None:
173+
self.imitated_config_db = super().get_config_db_as_json()
174+
158175

159176
class PatchWrapper:
160177
def __init__(self, config_wrapper=None):

tests/generic_config_updater/change_applier_test.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import os
55
import unittest
66
from collections import defaultdict
7-
from unittest.mock import patch
7+
from unittest.mock import patch, Mock, call
88

99
import generic_config_updater.change_applier
1010
import generic_config_updater.services_validator
@@ -269,4 +269,16 @@ def test_change_apply(self, mock_set, mock_db, mock_os_sys):
269269
debug_print("all good for applier")
270270

271271

272-
272+
class TestDryRunChangeApplier(unittest.TestCase):
273+
def test_apply__calls_apply_change_to_config_db(self):
274+
# Arrange
275+
change = Mock()
276+
config_wrapper = Mock()
277+
applier = generic_config_updater.change_applier.DryRunChangeApplier(config_wrapper)
278+
279+
# Act
280+
applier.apply(change)
281+
282+
# Assert
283+
applier.config_wrapper.apply_change_to_config_db.assert_has_calls([call(change)])
284+

tests/generic_config_updater/generic_updater_test.py

+11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import generic_config_updater.generic_updater as gu
99
import generic_config_updater.patch_sorter as ps
10+
import generic_config_updater.change_applier as ca
1011

1112
# import sys
1213
# sys.path.insert(0,'../../generic_config_updater')
@@ -420,8 +421,11 @@ def validate_create_patch_applier(self, params, expected_decorators):
420421
self.assertIsInstance(patch_applier, gu.PatchApplier)
421422
if params["dry_run"]:
422423
self.assertIsInstance(patch_applier.config_wrapper, gu.DryRunConfigWrapper)
424+
self.assertIsInstance(patch_applier.changeapplier, ca.DryRunChangeApplier)
425+
self.assertIsInstance(patch_applier.changeapplier.config_wrapper, gu.DryRunConfigWrapper)
423426
else:
424427
self.assertIsInstance(patch_applier.config_wrapper, gu.ConfigWrapper)
428+
self.assertIsInstance(patch_applier.changeapplier, ca.ChangeApplier)
425429

426430
if params["ignore_non_yang_tables"] or params["ignore_paths"]:
427431
self.assertIsInstance(patch_applier.patchsorter, ps.NonStrictPatchSorter)
@@ -451,9 +455,12 @@ def validate_create_config_replacer(self, params, expected_decorators):
451455
if params["dry_run"]:
452456
self.assertIsInstance(config_replacer.config_wrapper, gu.DryRunConfigWrapper)
453457
self.assertIsInstance(config_replacer.patch_applier.config_wrapper, gu.DryRunConfigWrapper)
458+
self.assertIsInstance(config_replacer.patch_applier.changeapplier, ca.DryRunChangeApplier)
459+
self.assertIsInstance(config_replacer.patch_applier.changeapplier.config_wrapper, gu.DryRunConfigWrapper)
454460
else:
455461
self.assertIsInstance(config_replacer.config_wrapper, gu.ConfigWrapper)
456462
self.assertIsInstance(config_replacer.patch_applier.config_wrapper, gu.ConfigWrapper)
463+
self.assertIsInstance(config_replacer.patch_applier.changeapplier, ca.ChangeApplier)
457464

458465
if params["ignore_non_yang_tables"] or params["ignore_paths"]:
459466
self.assertIsInstance(config_replacer.patch_applier.patchsorter, ps.NonStrictPatchSorter)
@@ -482,11 +489,15 @@ def validate_create_config_rollbacker(self, params, expected_decorators):
482489
self.assertIsInstance(config_rollbacker.config_replacer.config_wrapper, gu.DryRunConfigWrapper)
483490
self.assertIsInstance(
484491
config_rollbacker.config_replacer.patch_applier.config_wrapper, gu.DryRunConfigWrapper)
492+
self.assertIsInstance(config_rollbacker.config_replacer.patch_applier.changeapplier, ca.DryRunChangeApplier)
493+
self.assertIsInstance(
494+
config_rollbacker.config_replacer.patch_applier.changeapplier.config_wrapper, gu.DryRunConfigWrapper)
485495
else:
486496
self.assertIsInstance(config_rollbacker.config_wrapper, gu.ConfigWrapper)
487497
self.assertIsInstance(config_rollbacker.config_replacer.config_wrapper, gu.ConfigWrapper)
488498
self.assertIsInstance(
489499
config_rollbacker.config_replacer.patch_applier.config_wrapper, gu.ConfigWrapper)
500+
self.assertIsInstance(config_rollbacker.config_replacer.patch_applier.changeapplier, ca.ChangeApplier)
490501

491502
if params["ignore_non_yang_tables"] or params["ignore_paths"]:
492503
self.assertIsInstance(config_rollbacker.config_replacer.patch_applier.patchsorter, ps.NonStrictPatchSorter)

tests/generic_config_updater/gu_common_test.py

+44
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,50 @@
77
from .gutest_helpers import create_side_effect_dict, Files
88
import generic_config_updater.gu_common as gu_common
99

10+
class TestDryRunConfigWrapper(unittest.TestCase):
11+
def test_get_config_db_as_json__returns_imitated_config_db(self):
12+
# Arrange
13+
config_wrapper = gu_common.DryRunConfigWrapper(Files.CONFIG_DB_AS_JSON)
14+
expected = Files.CONFIG_DB_AS_JSON
15+
16+
# Act
17+
actual = config_wrapper.get_config_db_as_json()
18+
19+
# Assert
20+
self.assertDictEqual(expected, actual)
21+
22+
def test_get_sonic_yang_as_json__returns_imitated_config_db_as_yang(self):
23+
# Arrange
24+
config_wrapper = gu_common.DryRunConfigWrapper(Files.CONFIG_DB_AS_JSON)
25+
expected = Files.SONIC_YANG_AS_JSON
26+
27+
# Act
28+
actual = config_wrapper.get_sonic_yang_as_json()
29+
30+
# Assert
31+
self.assertDictEqual(expected, actual)
32+
33+
def test_apply_change_to_config_db__multiple_calls__changes_imitated_config_db(self):
34+
# Arrange
35+
imitated_config_db = Files.CONFIG_DB_AS_JSON
36+
config_wrapper = gu_common.DryRunConfigWrapper(imitated_config_db)
37+
38+
changes = [gu_common.JsonChange(jsonpatch.JsonPatch([{'op':'remove', 'path':'/VLAN'}])),
39+
gu_common.JsonChange(jsonpatch.JsonPatch([{'op':'remove', 'path':'/ACL_TABLE'}])),
40+
gu_common.JsonChange(jsonpatch.JsonPatch([{'op':'remove', 'path':'/PORT'}]))
41+
]
42+
43+
expected = imitated_config_db
44+
for change in changes:
45+
# Act
46+
config_wrapper.apply_change_to_config_db(change)
47+
48+
actual = config_wrapper.get_config_db_as_json()
49+
expected = change.apply(expected)
50+
51+
# Assert
52+
self.assertDictEqual(expected, actual)
53+
1054
class TestConfigWrapper(unittest.TestCase):
1155
def setUp(self):
1256
self.config_wrapper_mock = gu_common.ConfigWrapper()

0 commit comments

Comments
 (0)