Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GCU] Implementing DryRun by printing patch-sorter steps/imitating config_db #1973

Merged
merged 6 commits into from
Dec 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,10 @@ def load(filename, yes):
log.log_info("'load' executing...")
clicommon.run_command(command, display_cmd=True)

def print_dry_run_message(dry_run):
if dry_run:
click.secho("** DRY RUN EXECUTION **", fg="yellow", underline=True)

@config.command('apply-patch')
@click.argument('patch-file-path', type=str, required=True)
@click.option('-f', '--format', type=click.Choice([e.name for e in ConfigFormat]),
Expand All @@ -1182,6 +1186,8 @@ def apply_patch(ctx, patch_file_path, format, dry_run, ignore_non_yang_tables, i

<patch-file-path>: Path to the patch file on the file-system."""
try:
print_dry_run_message(dry_run)

with open(patch_file_path, 'r') as fh:
text = fh.read()
patch_as_json = json.loads(text)
Expand Down Expand Up @@ -1214,6 +1220,8 @@ def replace(ctx, target_file_path, format, dry_run, ignore_non_yang_tables, igno

<target-file-path>: Path to the target file on the file-system."""
try:
print_dry_run_message(dry_run)

with open(target_file_path, 'r') as fh:
target_config_as_text = fh.read()
target_config = json.loads(target_config_as_text)
Expand Down Expand Up @@ -1241,6 +1249,8 @@ def rollback(ctx, checkpoint_name, dry_run, ignore_non_yang_tables, ignore_path,

<checkpoint-name>: The checkpoint name, use `config list-checkpoints` command to see available checkpoints."""
try:
print_dry_run_message(dry_run)

GenericUpdater().rollback(checkpoint_name, verbose, dry_run, ignore_non_yang_tables, ignore_path)

click.secho("Config rolled back successfully.", fg="cyan", underline=True)
Expand Down
10 changes: 10 additions & 0 deletions generic_config_updater/change_applier.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ def prune_empty_table(data):
return data


class DryRunChangeApplier:

def __init__(self, config_wrapper):
self.config_wrapper = config_wrapper


def apply(self, change):
self.config_wrapper.apply_change_to_config_db(change)


class ChangeApplier:

updater_conf = None
Expand Down
26 changes: 22 additions & 4 deletions generic_config_updater/generic_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
DryRunConfigWrapper, PatchWrapper, genericUpdaterLogging
from .patch_sorter import StrictPatchSorter, NonStrictPatchSorter, ConfigSplitter, \
TablesWithoutYangConfigSplitter, IgnorePathsFromYangConfigSplitter
from .change_applier import ChangeApplier
from .change_applier import ChangeApplier, DryRunChangeApplier

CHECKPOINTS_DIR = "/etc/sonic/checkpoints"
CHECKPOINT_EXT = ".cp.json"
Expand Down Expand Up @@ -299,9 +299,13 @@ class GenericUpdateFactory:
def create_patch_applier(self, config_format, verbose, dry_run, ignore_non_yang_tables, ignore_paths):
self.init_verbose_logging(verbose)
config_wrapper = self.get_config_wrapper(dry_run)
change_applier = self.get_change_applier(dry_run, config_wrapper)
patch_wrapper = PatchWrapper(config_wrapper)
patch_sorter = self.get_patch_sorter(ignore_non_yang_tables, ignore_paths, config_wrapper, patch_wrapper)
patch_applier = PatchApplier(config_wrapper=config_wrapper, patchsorter=patch_sorter, patch_wrapper=patch_wrapper)
patch_applier = PatchApplier(config_wrapper=config_wrapper,
patchsorter=patch_sorter,
patch_wrapper=patch_wrapper,
changeapplier=change_applier)

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

config_wrapper = self.get_config_wrapper(dry_run)
change_applier = self.get_change_applier(dry_run, config_wrapper)
patch_wrapper = PatchWrapper(config_wrapper)
patch_sorter = self.get_patch_sorter(ignore_non_yang_tables, ignore_paths, config_wrapper, patch_wrapper)
patch_applier = PatchApplier(config_wrapper=config_wrapper, patchsorter=patch_sorter, patch_wrapper=patch_wrapper)
patch_applier = PatchApplier(config_wrapper=config_wrapper,
patchsorter=patch_sorter,
patch_wrapper=patch_wrapper,
changeapplier=change_applier)

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

config_wrapper = self.get_config_wrapper(dry_run)
change_applier = self.get_change_applier(dry_run, config_wrapper)
patch_wrapper = PatchWrapper(config_wrapper)
patch_sorter = self.get_patch_sorter(ignore_non_yang_tables, ignore_paths, config_wrapper, patch_wrapper)
patch_applier = PatchApplier(config_wrapper=config_wrapper, patchsorter=patch_sorter, patch_wrapper=patch_wrapper)
patch_applier = PatchApplier(config_wrapper=config_wrapper,
patchsorter=patch_sorter,
patch_wrapper=patch_wrapper,
changeapplier=change_applier)

config_replacer = ConfigReplacer(config_wrapper=config_wrapper, patch_applier=patch_applier)
config_rollbacker = FileSystemConfigRollbacker(config_wrapper = config_wrapper, config_replacer = config_replacer)
Expand All @@ -363,6 +375,12 @@ def get_config_wrapper(self, dry_run):
else:
return ConfigWrapper()

def get_change_applier(self, dry_run, config_wrapper):
if dry_run:
return DryRunChangeApplier(config_wrapper)
else:
return ChangeApplier()

def get_patch_sorter(self, ignore_non_yang_tables, ignore_paths, config_wrapper, patch_wrapper):
if not ignore_non_yang_tables and not ignore_paths:
return StrictPatchSorter(config_wrapper, patch_wrapper)
Expand Down
21 changes: 19 additions & 2 deletions generic_config_updater/gu_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,26 @@ def remove_empty_tables(self, config):
return config_with_non_empty_tables

class DryRunConfigWrapper(ConfigWrapper):
# TODO: implement DryRunConfigWrapper
# This class will simulate all read/write operations to ConfigDB on a virtual storage unit.
pass
def __init__(self, initial_imitated_config_db = None):
super().__init__()
self.logger = genericUpdaterLogging.get_logger(title="** DryRun", print_all_to_console=True)
self.imitated_config_db = copy.deepcopy(initial_imitated_config_db)

def apply_change_to_config_db(self, change):
self._init_imitated_config_db_if_none()
self.logger.log_notice(f"Would apply {change}")
self.imitated_config_db = change.apply(self.imitated_config_db)

def get_config_db_as_json(self):
self._init_imitated_config_db_if_none()
return self.imitated_config_db

def _init_imitated_config_db_if_none(self):
# if there is no initial imitated config_db and it is the first time calling this method
if self.imitated_config_db is None:
self.imitated_config_db = super().get_config_db_as_json()


class PatchWrapper:
def __init__(self, config_wrapper=None):
Expand Down
16 changes: 14 additions & 2 deletions tests/generic_config_updater/change_applier_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import os
import unittest
from collections import defaultdict
from unittest.mock import patch
from unittest.mock import patch, Mock, call

import generic_config_updater.change_applier
import generic_config_updater.services_validator
Expand Down Expand Up @@ -269,4 +269,16 @@ def test_change_apply(self, mock_set, mock_db, mock_os_sys):
debug_print("all good for applier")



class TestDryRunChangeApplier(unittest.TestCase):
def test_apply__calls_apply_change_to_config_db(self):
# Arrange
change = Mock()
config_wrapper = Mock()
applier = generic_config_updater.change_applier.DryRunChangeApplier(config_wrapper)

# Act
applier.apply(change)

# Assert
applier.config_wrapper.apply_change_to_config_db.assert_has_calls([call(change)])

11 changes: 11 additions & 0 deletions tests/generic_config_updater/generic_updater_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import generic_config_updater.generic_updater as gu
import generic_config_updater.patch_sorter as ps
import generic_config_updater.change_applier as ca

# import sys
# sys.path.insert(0,'../../generic_config_updater')
Expand Down Expand Up @@ -420,8 +421,11 @@ def validate_create_patch_applier(self, params, expected_decorators):
self.assertIsInstance(patch_applier, gu.PatchApplier)
if params["dry_run"]:
self.assertIsInstance(patch_applier.config_wrapper, gu.DryRunConfigWrapper)
self.assertIsInstance(patch_applier.changeapplier, ca.DryRunChangeApplier)
self.assertIsInstance(patch_applier.changeapplier.config_wrapper, gu.DryRunConfigWrapper)
else:
self.assertIsInstance(patch_applier.config_wrapper, gu.ConfigWrapper)
self.assertIsInstance(patch_applier.changeapplier, ca.ChangeApplier)

if params["ignore_non_yang_tables"] or params["ignore_paths"]:
self.assertIsInstance(patch_applier.patchsorter, ps.NonStrictPatchSorter)
Expand Down Expand Up @@ -451,9 +455,12 @@ def validate_create_config_replacer(self, params, expected_decorators):
if params["dry_run"]:
self.assertIsInstance(config_replacer.config_wrapper, gu.DryRunConfigWrapper)
self.assertIsInstance(config_replacer.patch_applier.config_wrapper, gu.DryRunConfigWrapper)
self.assertIsInstance(config_replacer.patch_applier.changeapplier, ca.DryRunChangeApplier)
self.assertIsInstance(config_replacer.patch_applier.changeapplier.config_wrapper, gu.DryRunConfigWrapper)
else:
self.assertIsInstance(config_replacer.config_wrapper, gu.ConfigWrapper)
self.assertIsInstance(config_replacer.patch_applier.config_wrapper, gu.ConfigWrapper)
self.assertIsInstance(config_replacer.patch_applier.changeapplier, ca.ChangeApplier)

if params["ignore_non_yang_tables"] or params["ignore_paths"]:
self.assertIsInstance(config_replacer.patch_applier.patchsorter, ps.NonStrictPatchSorter)
Expand Down Expand Up @@ -482,11 +489,15 @@ def validate_create_config_rollbacker(self, params, expected_decorators):
self.assertIsInstance(config_rollbacker.config_replacer.config_wrapper, gu.DryRunConfigWrapper)
self.assertIsInstance(
config_rollbacker.config_replacer.patch_applier.config_wrapper, gu.DryRunConfigWrapper)
self.assertIsInstance(config_rollbacker.config_replacer.patch_applier.changeapplier, ca.DryRunChangeApplier)
self.assertIsInstance(
config_rollbacker.config_replacer.patch_applier.changeapplier.config_wrapper, gu.DryRunConfigWrapper)
else:
self.assertIsInstance(config_rollbacker.config_wrapper, gu.ConfigWrapper)
self.assertIsInstance(config_rollbacker.config_replacer.config_wrapper, gu.ConfigWrapper)
self.assertIsInstance(
config_rollbacker.config_replacer.patch_applier.config_wrapper, gu.ConfigWrapper)
self.assertIsInstance(config_rollbacker.config_replacer.patch_applier.changeapplier, ca.ChangeApplier)

if params["ignore_non_yang_tables"] or params["ignore_paths"]:
self.assertIsInstance(config_rollbacker.config_replacer.patch_applier.patchsorter, ps.NonStrictPatchSorter)
Expand Down
44 changes: 44 additions & 0 deletions tests/generic_config_updater/gu_common_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,50 @@
from .gutest_helpers import create_side_effect_dict, Files
import generic_config_updater.gu_common as gu_common

class TestDryRunConfigWrapper(unittest.TestCase):
def test_get_config_db_as_json__returns_imitated_config_db(self):
# Arrange
config_wrapper = gu_common.DryRunConfigWrapper(Files.CONFIG_DB_AS_JSON)
expected = Files.CONFIG_DB_AS_JSON

# Act
actual = config_wrapper.get_config_db_as_json()

# Assert
self.assertDictEqual(expected, actual)

def test_get_sonic_yang_as_json__returns_imitated_config_db_as_yang(self):
# Arrange
config_wrapper = gu_common.DryRunConfigWrapper(Files.CONFIG_DB_AS_JSON)
expected = Files.SONIC_YANG_AS_JSON

# Act
actual = config_wrapper.get_sonic_yang_as_json()

# Assert
self.assertDictEqual(expected, actual)

def test_apply_change_to_config_db__multiple_calls__changes_imitated_config_db(self):
# Arrange
imitated_config_db = Files.CONFIG_DB_AS_JSON
config_wrapper = gu_common.DryRunConfigWrapper(imitated_config_db)

changes = [gu_common.JsonChange(jsonpatch.JsonPatch([{'op':'remove', 'path':'/VLAN'}])),
gu_common.JsonChange(jsonpatch.JsonPatch([{'op':'remove', 'path':'/ACL_TABLE'}])),
gu_common.JsonChange(jsonpatch.JsonPatch([{'op':'remove', 'path':'/PORT'}]))
]

expected = imitated_config_db
for change in changes:
# Act
config_wrapper.apply_change_to_config_db(change)

actual = config_wrapper.get_config_db_as_json()
expected = change.apply(expected)

# Assert
self.assertDictEqual(expected, actual)

class TestConfigWrapper(unittest.TestCase):
def setUp(self):
self.config_wrapper_mock = gu_common.ConfigWrapper()
Expand Down