Skip to content
103 changes: 103 additions & 0 deletions generic_config_updater/change_applier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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():
config_db = ConfigDBConnector()
config_db.connect()
return config_db


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, 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, {}), 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):
(_, fname) = tempfile.mkstemp(suffix="_changeApplier")
os.system("sonic-cfggen -d --print-data > {}".format(fname))
run_data = {}
with open(fname, "r") as s:
run_data = json.load(s)
if os.path.isfile(fname):
os.remove(fname)
return run_data





13 changes: 7 additions & 6 deletions generic_config_updater/generic_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
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


CHECKPOINTS_DIR = "/etc/sonic/checkpoints"
CHECKPOINT_EXT = ".cp.json"
Expand All @@ -17,15 +21,12 @@ def release_lock(self):
# TODO: Implement ConfigLock
pass

class ChangeApplier:
def apply(self, change):
# TODO: Implement change applier
raise NotImplementedError("ChangeApplier.apply(change) is not implemented yet")

class ConfigFormat(Enum):
CONFIGDB = 1
SONICYANG = 2


class PatchApplier:
def __init__(self,
patchsorter=None,
Expand Down Expand Up @@ -297,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
Loading