Skip to content

Commit 0b2536b

Browse files
Generic_upater: Apply JSON change (#1856)
What I did Apply JSON change How I did it Get running config, apply json change and set the updates onto running redis.
1 parent 8ea834b commit 0b2536b

File tree

7 files changed

+819
-4
lines changed

7 files changed

+819
-4
lines changed
+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import copy
2+
import json
3+
import jsondiff
4+
import importlib
5+
import os
6+
import tempfile
7+
from collections import defaultdict
8+
from swsscommon.swsscommon import ConfigDBConnector
9+
from .gu_common import genericUpdaterLogging
10+
11+
12+
UPDATER_CONF_FILE = "/etc/sonic/generic_config_updater.conf"
13+
logger = genericUpdaterLogging.get_logger(title="Change Applier")
14+
15+
print_to_console = False
16+
print_to_stdout = False
17+
18+
def set_print_options(to_console=False, to_stdout=False):
19+
global print_to_console, print_to_stdout
20+
21+
print_to_console = to_console
22+
print_to_stdout = to_stdout
23+
24+
25+
def log_debug(m):
26+
logger.log_debug(m, print_to_console)
27+
if print_to_stdout:
28+
print(m)
29+
30+
31+
def log_error(m):
32+
logger.log_error(m, print_to_console)
33+
if print_to_stdout:
34+
print(m)
35+
36+
37+
def get_config_db():
38+
config_db = ConfigDBConnector()
39+
config_db.connect()
40+
return config_db
41+
42+
43+
def set_config(config_db, tbl, key, data):
44+
config_db.set_entry(tbl, key, data)
45+
46+
47+
class ChangeApplier:
48+
49+
updater_conf = None
50+
51+
def __init__(self):
52+
self.config_db = get_config_db()
53+
if (not ChangeApplier.updater_conf) and os.path.exists(UPDATER_CONF_FILE):
54+
with open(UPDATER_CONF_FILE, "r") as s:
55+
ChangeApplier.updater_conf = json.load(s)
56+
57+
58+
def _invoke_cmd(self, cmd, old_cfg, upd_cfg, keys):
59+
# cmd is in the format as <package/module name>.<method name>
60+
#
61+
method_name = cmd.split(".")[-1]
62+
module_name = ".".join(cmd.split(".")[0:-1])
63+
64+
module = importlib.import_module(module_name, package=None)
65+
method_to_call = getattr(module, method_name)
66+
67+
return method_to_call(old_cfg, upd_cfg, keys)
68+
69+
70+
def _services_validate(self, old_cfg, upd_cfg, keys):
71+
lst_svcs = set()
72+
lst_cmds = set()
73+
if not keys:
74+
# calling apply with no config would invoke
75+
# default validation, if any
76+
#
77+
keys[""] = {}
78+
79+
tables = ChangeApplier.updater_conf["tables"]
80+
for tbl in keys:
81+
lst_svcs.update(tables.get(tbl, {}).get("services_to_validate", []))
82+
83+
services = ChangeApplier.updater_conf["services"]
84+
for svc in lst_svcs:
85+
lst_cmds.update(services.get(svc, {}).get("validate_commands", []))
86+
87+
for cmd in lst_cmds:
88+
ret = self._invoke_cmd(cmd, old_cfg, upd_cfg, keys)
89+
if ret:
90+
log_error("service invoked: {} failed with ret={}".format(cmd, ret))
91+
return ret
92+
log_debug("service invoked: {}".format(cmd))
93+
return 0
94+
95+
96+
def _upd_data(self, tbl, run_tbl, upd_tbl, upd_keys):
97+
for key in set(run_tbl.keys()).union(set(upd_tbl.keys())):
98+
run_data = run_tbl.get(key, None)
99+
upd_data = upd_tbl.get(key, None)
100+
101+
if run_data != upd_data:
102+
set_config(self.config_db, tbl, key, upd_data)
103+
upd_keys[tbl][key] = {}
104+
log_debug("Patch affected tbl={} key={}".format(tbl, key))
105+
106+
107+
def _report_mismatch(self, run_data, upd_data):
108+
log_error("run_data vs expected_data: {}".format(
109+
str(jsondiff.diff(run_data, upd_data))[0:40]))
110+
111+
112+
def apply(self, change):
113+
run_data = self._get_running_config()
114+
upd_data = change.apply(copy.deepcopy(run_data))
115+
upd_keys = defaultdict(dict)
116+
117+
for tbl in sorted(set(run_data.keys()).union(set(upd_data.keys()))):
118+
self._upd_data(tbl, run_data.get(tbl, {}),
119+
upd_data.get(tbl, {}), upd_keys)
120+
121+
ret = self._services_validate(run_data, upd_data, upd_keys)
122+
if not ret:
123+
run_data = self._get_running_config()
124+
if upd_data != run_data:
125+
self._report_mismatch(run_data, upd_data)
126+
ret = -1
127+
if ret:
128+
log_error("Failed to apply Json change")
129+
return ret
130+
131+
132+
def _get_running_config(self):
133+
(_, fname) = tempfile.mkstemp(suffix="_changeApplier")
134+
os.system("sonic-cfggen -d --print-data > {}".format(fname))
135+
run_data = {}
136+
with open(fname, "r") as s:
137+
run_data = json.load(s)
138+
if os.path.isfile(fname):
139+
os.remove(fname)
140+
return run_data

generic_config_updater/generic_updater.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .gu_common import GenericConfigUpdaterError, ConfigWrapper, \
55
DryRunConfigWrapper, PatchWrapper, genericUpdaterLogging
66
from .patch_sorter import PatchSorter
7+
from .change_applier import ChangeApplier
78

89
CHECKPOINTS_DIR = "/etc/sonic/checkpoints"
910
CHECKPOINT_EXT = ".cp.json"
@@ -17,10 +18,6 @@ def release_lock(self):
1718
# TODO: Implement ConfigLock
1819
pass
1920

20-
class ChangeApplier:
21-
def apply(self, change):
22-
# TODO: Implement change applier
23-
raise NotImplementedError("ChangeApplier.apply(change) is not implemented yet")
2421

2522
class ConfigFormat(Enum):
2623
CONFIGDB = 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"tables": {
3+
"": {
4+
"services_to_validate": [ "system_health" ]
5+
},
6+
"PORT": {
7+
"services_to_validate": [ "port_service" ]
8+
},
9+
"SYSLOG_SERVER":{
10+
"services_to_validate": [ "rsyslog" ]
11+
},
12+
"DHCP_RELAY": {
13+
"services_to_validate": [ "dhcp-relay" ]
14+
},
15+
"DHCP_SERVER": {
16+
"services_to_validate": [ "dhcp-relay" ]
17+
}
18+
},
19+
"README": [
20+
"Validate_commands provides, module & method name as ",
21+
" <module name>.<method name>",
22+
"NOTE: module name could have '.'",
23+
" ",
24+
"The last element separated by '.' is considered as ",
25+
"method name",
26+
"",
27+
"e.g. 'show.acl.test_acl'",
28+
"",
29+
"Here we load 'show.acl' and call 'test_acl' method on it.",
30+
"",
31+
"called as:",
32+
" <module>.<method>>(<config before change>, ",
33+
" <config after change>, <affected keys>)",
34+
" config is in JSON format as in config_db.json",
35+
" affected_keys in same format, but w/o value",
36+
" { 'ACL_TABLE': { 'SNMP_ACL': {} ... }, ...}",
37+
" The affected keys has 'added', 'updated' & 'deleted'",
38+
"",
39+
"Multiple validate commands may be provided.",
40+
"",
41+
"Note: The commands may be called in any order",
42+
""
43+
],
44+
"services": {
45+
"system_health": {
46+
"validate_commands": [ ]
47+
},
48+
"port_service": {
49+
"validate_commands": [ ]
50+
},
51+
"rsyslog": {
52+
"validate_commands": [ "services_validator.ryslog_validator" ]
53+
},
54+
"dhcp-relay": {
55+
"validate_commands": [ "services_validator.dhcp_validator" ]
56+
}
57+
}
58+
}
59+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import os
2+
from .gu_common import genericUpdaterLogging
3+
4+
logger = genericUpdaterLogging.get_logger(title="Service Validator")
5+
6+
def _service_restart(svc_name):
7+
os.system(f"systemctl restart {svc_name}")
8+
logger.log_notice(f"Restarted {svc_name}")
9+
10+
11+
def ryslog_validator(old_config, upd_config, keys):
12+
_service_restart("rsyslog-config")
13+
14+
15+
def dhcp_validator(old_config, upd_config, keys):
16+
_service_restart("dhcp_relay")
17+

0 commit comments

Comments
 (0)