Skip to content

Commit

Permalink
Added service validation
Browse files Browse the repository at this point in the history
  • Loading branch information
renukamanavalan committed Oct 11, 2021
1 parent dd337eb commit 6e0bba0
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 7 deletions.
55 changes: 53 additions & 2 deletions generic_config_updater/change_applier.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import copy
import json
import importlib
import os
import syslog
import tempfile
from collections import defaultdict
from swsscommon.swsscommon import ConfigDBConnector
from .gu_common import log_error, log_debug, log_info


def get_config_db():
Expand All @@ -15,25 +19,72 @@ def set_config(config_db, tbl, key, data):
config_db.set_entry(tbl, key, data)


UPDATER_CONF_FILE = "/etc/sonic/generic_config_updater.conf"
updater_data = None

class ChangeApplier:
def __init__(self):
global updater_data, log_level

self.config_db = get_config_db()
if updater_data == None:
with open(UPDATER_CONF_FILE, "r") as s:
updater_data = json.load(s)


def _invoke_cmd(cmd, old_cfg, upd_cfg, keys):
method_name = cmd.split(".")[-1]
module_name = ".".join(cmd.split(".")[0:-1])

module = importlib.import_module(module_name, package=None)
method_to_call = getattr(module, method_name)

return method_to_call(old_cfg, upd_cfg, keys)


def _services_validate(old_cfg, upd_cfg, keys):
lst_svcs = set()
lst_cmds = set()
if not keys:
keys[""] = {}
for tbl in keys:
lst_svcs.update(updater_data.get(tbl, {}).get("services_to_validate", []))
for svc in lst_svcs:
lst_cmds.update(updater_data.get(svc, {}).get("validate_commands", []))

for cmd in lst_cmds:
ret = _invoke_cmd(cmd, old_cfg, upd_cfg, keys)
if ret:
return ret
return 0


def _upd_data(self, tbl, run_tbl, upd_tbl):
def _upd_data(self, tbl, run_tbl, upd_tbl, upd_keys):
for key in set(run_tbl.keys()).union(set(upd_tbl.keys())):
run_data = run_tbl.get(key, None)
upd_data = upd_tbl.get(key, None)

if run_data != upd_data:
set_config(self.config_db, tbl, key, upd_data)
upd_keys[tbl][key] = {}


def apply(self, change):
run_data = self._get_running_config()
upd_data = change.apply(copy.deepcopy(run_data))
upd_keys = defaultdict(dict)

for tbl in set(run_data.keys()).union(set(upd_data.keys())):
self._upd_data(tbl, run_data.get(tbl, {}), upd_data.get(tbl, {}))
self._upd_data(tbl, run_data.get(tbl, {}),
upd_data.get(tbl, {}), upd_keys)

ret = _services_validate(run_data, upd_data, upd_keys)
if not ret:
run_data = self._get_running_config()
if upd_data != run_data:
report_mismatch(run_data, upd_data)
ret = -1
return ret


def _get_running_config(self):
Expand Down
6 changes: 4 additions & 2 deletions generic_config_updater/generic_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
import os
from enum import Enum
from .gu_common import GenericConfigUpdaterError, ConfigWrapper, \
DryRunConfigWrapper, PatchWrapper
DryRunConfigWrapper, PatchWrapper, \
set_log_level

from .patch_sorter import PatchSorter
from .change_applier import ChangeApplier

Expand Down Expand Up @@ -296,7 +298,7 @@ def init_verbose_logging(self, verbose):
# Usually logs have levels such as: error, warning, info, debug.
# By default all log levels should show up to the user, except debug.
# By allowing verbose logging, debug msgs will also be shown to the user.
pass
set_log_level(syslog.LOG_ERR if not verbose else syslog.LOG_DEBUG)

def get_config_wrapper(self, dry_run):
if dry_run:
Expand Down
44 changes: 44 additions & 0 deletions generic_config_updater/generic_updater_config.conf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"tables": {
"": {
"services_to_validate": [ "system_health" ]
},
"PORT": {
"services_to_validate": [ "port_service" ]
}
},
"README": [
'Validate_commands provides, module & method name as ',
' <module name>.<method name>',
'NOTE: module name could have "."',
' ',
'The last element separated by "." is considered as ',
'method name',
'',
'e.g. "show.acl.test_acl"',
'',
'Here we load "show.acl" and call "test_acl" method on it.',
'',
'called as:',
' <module>.<method>>(<config before change>, ',
' <config after change>, <affected keys>)',
' config is in JSON format as in config_db.json',
' affected_keys in same format, but w/o value',
' { "ACL_TABLE": { "SNMP_ACL": {} ... }, ...}',
' The affected keys has "added", "updated" & "deleted"',
'',
'Multiple validate commands may be provided.',',
'',
'Note: The commands may be called in any order',
''
],
"services": {
"system_health": {
"validate_commands": [ ]
},
"port_service": {
"validate_commands": [ ]
}
}
}

30 changes: 30 additions & 0 deletions generic_config_updater/gu_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,36 @@
class GenericConfigUpdaterError(Exception):
pass

log_level = syslog.LOG_ERR

def _log_msg(lvl, m):
if lvl <= log_level:
syslog.syslog(lvl, m)
if log_level == syslog.LOG_DEBUG:
print(m)

def log_error(m):
_log_msg(syslog.LOG_ERR, m)


def log_info(m):
_log_msg(syslog.LOG_INFO, m)


def log_debug(m):
_log_msg(syslog.LOG_DEBUG, m)


def run_cmd(cmd):
proc = subprocess.run(cmd, shell=True, capture_output=True)
if proc.returncode:
log_error("Failed to run: ret={} cmd: {}".format(
proc.returncode, proc.args))
log_error(f"Failed to run: stdout: {proc.stdout}")
log_error(f"Failed to run: stderr: {proc.stderr}")
return proc.returncode


class JsonChange:
"""
A class that describes a partial change to a JSON object.
Expand Down
108 changes: 105 additions & 3 deletions tests/generic_config_updater/change_applier_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,60 @@
import json
import os
import unittest
from collections import defaultdict
from unittest.mock import patch

import generic_config_updater.change_applier
import generic_config_updater.gu_common

SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
DATA_FILE = os.path.join(SCRIPT_DIR, "files", "change_applier_test.data.json")
CONF_FILE = os.path.join(SCRIPT_DIR, "files", "change_applier_test.conf.json")
#
# Datafile is structured as
# "running_config": {....}
# "json_changes": [ {"name": ..., "update": { <tbl>: {<key>: {<new data>}, ...}...},
# "remove": { <tbl>: { <key>: {}, ..}, ...} }, ...]
#
# The json_changes is read into global json_changes
# The applier is called with each change
# The mocked JsonChange.apply applies this diff on given config
# The applier calls set_entry to update redis
# But we mock set_entry, and that instead:
# remove the corresponding changes from json_changes.
# Updates the global running_config
#
# At the end of application of all changes, expect global json-changes to
# be empty, which assures that set_entry is called for all expected keys.
# The global running config would reflect the final config
#
# The changes are written in such a way, upon the last change, the config
# will be same as the original config that we started with or as read from
# data file.
#
# So compare global running_config with read_data for running config
# from the file.
# This compares the integrity of final o/p

# Data read from file
read_data = {}

# Keep a copy of running_config before calling apply
# This is used by service_validate call to verify the args
# Args from change applier: (<config before change> <config after change>
# <affected keys>
start_running_config = {}

# The mock_set_entry (otherwise redis update) reflects the final config
# service_validate calls will verify <config after change > against this
#
running_config = {}

# Copy of changes read. Used by mock JsonChange.apply
# Cleared by mocked set_entry
json_changes = {}

# The index into list of i/p json changes for mock code to use
json_change_index = 0

DB_HANDLE = "config_db"
Expand Down Expand Up @@ -115,13 +158,64 @@ def print_diff(expect, ct):
debug_print("diff is complete")


# Test validators
#
def system_health(old_cfg, new_cfg, keys):
svc_name = "system_health"
if old_cfg != new_cfg:
print_diff(old_cfg, new_cfg)
assert False, "No change expected"
svcs = json_changes[json_change_index].get("services_validated", None)
if svcs != None:
assert svc_name not in svcs
svcs.remove(svc_name)


def _validate_keys(keys):
change = copy.deepcopy(read_data["json_changes"][json_change_index])
change.update(read_data["json_changes"][json_change_index])

for tbl in set(change.keys()).union(set(keys.keys())):
assert tbl in change
assert tbl in keys
chg_tbl = change[tbl]
keys_tbl = keys[tbl]
for key in set(chg_tbl.keys()).union(set(keys_tbl.keys())):
assert key not in chg_tbl
assert key not in keys_tbl


def _validate_svc(svc_name, old_cfg, new_cfg, keys):
if old_cfg != start_running_config:
print_diff(old_cfg, start_running_config)
assert False

if new_cfg != running_config:
print_diff(old_cfg, running_config)
assert False

_validate_keys(keys)

svcs = json_changes[json_change_index].get("services_validated", None)
if svcs != None:
assert svc_name not in svcs
svcs.remove(svc_name)


def acl_validate(old_cfg, new_cfg, keys):
_validate_svc("acl_validate", old_cfg, new_cfg, keys)


def vlan_validate(old_cfg, new_cfg, keys):
_validate_svc("vlan_validate", old_cfg, new_cfg, keys)


class TestChangeApplier(unittest.TestCase):

@patch("generic_config_updater.gu_common.subprocess.run")
@patch("generic_config_updater.change_applier.os.system")
@patch("generic_config_updater.change_applier.get_config_db")
@patch("generic_config_updater.change_applier.set_config")
def test_application(self, mock_set, mock_db, mock_os_sys):
global read_data, running_config, json_changes, json_change_index

mock_os_sys.side_effect = os_system_cfggen
Expand All @@ -134,21 +228,29 @@ def test_application(self, mock_set, mock_db, mock_os_sys):
running_config = copy.deepcopy(read_data["running_data"])
json_changes = copy.deepcopy(read_data["json_changes"])

generic_config_updater.change_applier.UPDATER_CONF_FILE = CONF_FILE

applier = generic_config_updater.change_applier.ChangeApplier()
debug_print("invoked applier")

for i in range(len(json_changes)):
json_change_index = i

# Take copy for comparison
start_running_config = copy.deepcopy(running_config)

debug_print("main: json_change_index={}".format(json_change_index))
applier.apply(mock_obj())

# All changes are consumed
for i in range(len(json_changes)):
debug_print("Checking: index={} update:{} remove:{}".format(i,
debug_print("Checking: index={} update:{} remove:{} svcs:{}".format(i,
json.dumps(json_changes[i]["update"])[0:20],
json.dumps(json_changes[i]["remove"])[0:20]))
json.dumps(json_changes[i]["remove"])[0:20],
json.dumps(json_changes[i].get("services_validated", []))[0:20]))
assert not json_changes[i]["update"]
assert not json_changes[i]["remove"]
assert not json_changes[i].get("services_validated", [])

# Test data is set up in such a way the multiple changes
# finally brings it back to original config.
Expand Down
24 changes: 24 additions & 0 deletions tests/generic_config_updater/files/change_applier_test.conf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"tables": {
"": {
"services_to_validate": [ "system_health" ]
},
"PORT": {
"services_to_validate": [ "port_service" ]
},
"VLAN_INTERFACE": {
"services_to_validate": [ "port_service", "vlan_service" ]
}
},
"services": {
"system_health": {
"validate_commands": [ "..tests.generic_config_updater.change_applier_test.system_health" ]
}
"ACL_TABLE": {
"validate_commands": [ "..tests.generic_config_updater.change_applier_test.acl_validate" ]
},
"VLAN_INTERFACE": {
"validate_commands": [ "..tests.generic_config_updater.change_applier_test.vlan_validate" ]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,11 @@
}
},
"json_changes": [
{
"name": "change_0",
"update": {},
"remove": {}
},
{
"name": "change_1",
"update": {
Expand Down

0 comments on commit 6e0bba0

Please sign in to comment.