Skip to content

Commit 9ae6f6b

Browse files
authored
[debug dump util] Match Infrastructure (#1666)
What I did HLD for Dump Utility: HLD. Added the Logic for Match Infrastructure along with corresponding unit tests. Note: Before merging other PR's, please merge this first For More Info on MatchRequest and MatchEngine, Check this section in the HLD: MatchInfra
1 parent 8fe7e26 commit 9ae6f6b

File tree

6 files changed

+686
-0
lines changed

6 files changed

+686
-0
lines changed

dump/__init__.py

Whitespace-only changes.

dump/helper.py

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import os, sys
2+
3+
def create_template_dict(dbs):
4+
""" Generate a Template which will be returned by Executor Classes """
5+
return {db: {'keys': [], 'tables_not_found': []} for db in dbs}
6+
7+
def verbose_print(str):
8+
if "VERBOSE" in os.environ and os.environ["VERBOSE"] == "1":
9+
print(str)
10+
11+
def handle_error(err_str, excep=False):
12+
"""
13+
Handles general error conditions, if any experienced by the module,
14+
Set excep = True, to raise a exception
15+
"""
16+
if excep:
17+
raise Exception("ERROR : {}".format(err_str))
18+
else:
19+
print("ERROR : {}".format(err_str), file = sys.stderr)
20+
21+
22+
def handle_multiple_keys_matched_error(err_str, key_to_go_with="", excep=False):
23+
if excep:
24+
handle_error(err_str, True)
25+
else:
26+
print("ERROR (AMBIGUITY): {} \n Proceeding with the key {}".format(err_str, key_to_go_with), file = sys.stderr)
27+
28+
29+
def sort_lists(ret_template):
30+
""" Used to sort the nested list returned by the template dict. """
31+
for db in ret_template.keys():
32+
for key in ret_template[db].keys():
33+
if isinstance(ret_template[db][key], list):
34+
ret_template[db][key].sort()
35+
return ret_template

dump/match_infra.py

+300
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
import json, fnmatch
2+
from abc import ABC, abstractmethod
3+
from dump.helper import verbose_print
4+
from swsscommon.swsscommon import SonicV2Connector, SonicDBConfig
5+
from sonic_py_common import multi_asic
6+
from utilities_common.constants import DEFAULT_NAMESPACE
7+
8+
EXCEP_DICT = {
9+
"INV_REQ": "Argument should be of type MatchRequest",
10+
"INV_DB": "DB provided is not valid",
11+
"NO_MATCHES": "No Entries found for Table|key_pattern provided",
12+
"NO_SRC": "Either one of db or file in the request should be non-empty",
13+
"NO_TABLE": "No 'table' name provided",
14+
"NO_KEY": "'key_pattern' cannot be empty",
15+
"NO_VALUE" : "Field is provided, but no value is provided to compare with",
16+
"SRC_VAGUE": "Only one of db or file should be provided",
17+
"CONN_ERR" : "Connection Error",
18+
"JUST_KEYS_COMPAT": "When Just_keys is set to False, return_fields should be empty",
19+
"BAD_FORMAT_RE_FIELDS": "Return Fields should be of list type",
20+
"NO_ENTRIES": "No Keys found after applying the filtering criteria",
21+
"FILE_R_EXEP": "Exception Caught While Reading the json cfg file provided",
22+
"INV_NS": "Namespace is invalid"
23+
}
24+
25+
class MatchRequest:
26+
"""
27+
Request Object which should be passed to the MatchEngine
28+
29+
Attributes:
30+
"table" : A Valid Table Name
31+
"key_pattern" : Pattern of the redis-key to match. Defaults to "*". Eg: "*" will match all the keys.
32+
Supports these glob style patterns. https://redis.io/commands/KEYS
33+
"field" : Field to check for a match,Defaults to None
34+
"value" : Value to match, Defaults to None
35+
"return_fields" : An iterable type, where each element woudld imply a field to return from all the filtered keys
36+
"db" : A Valid DB name, Defaults to "".
37+
"file" : A Valid Config JSON file, Eg: copp_cfg.json, Defaults to "".
38+
Only one of the db/file fields should have a non-empty string.
39+
"just_keys" : If true, Only Returns the keys matched. Does not return field-value pairs. Defaults to True
40+
"ns" : namespace argument, if nothing is provided, default namespace is used
41+
"match_entire_list" : When this arg is set to true, entire list is matched incluing the ",".
42+
When False, the values are split based on "," and individual items are matched with
43+
"""
44+
def __init__(self, **kwargs):
45+
self.table = kwargs["table"] if "table" in kwargs else None
46+
self.key_pattern = kwargs["key_pattern"] if "key_pattern" in kwargs else "*"
47+
self.field = kwargs["field"] if "field" in kwargs else None
48+
self.value = kwargs["value"] if "value" in kwargs else None
49+
self.return_fields = kwargs["return_fields"] if "return_fields" in kwargs else []
50+
self.db = kwargs["db"] if "db" in kwargs else ""
51+
self.file = kwargs["file"] if "file" in kwargs else ""
52+
self.just_keys = kwargs["just_keys"] if "just_keys" in kwargs else True
53+
self.ns = kwargs["ns"] if "ns" in kwargs else ""
54+
self.match_entire_list = kwargs["match_entire_list"] if "match_entire_list" in kwargs else False
55+
err = self.__static_checks()
56+
verbose_print(str(err))
57+
if err:
58+
raise Exception("Static Checks for the MatchRequest Failed, Reason: \n" + err)
59+
60+
61+
def __static_checks(self):
62+
63+
if not self.db and not self.file:
64+
return EXCEP_DICT["NO_SRC"]
65+
66+
if self.db and self.file:
67+
return EXCEP_DICT["SRC_VAGUE"]
68+
69+
if not self.db:
70+
try:
71+
with open(self.file) as f:
72+
json.load(f)
73+
except Exception as e:
74+
return EXCEP_DICT["FILE_R_EXEP"] + str(e)
75+
76+
if not self.file and self.db not in SonicDBConfig.getDbList():
77+
return EXCEP_DICT["INV_DB"]
78+
79+
if not self.table:
80+
return EXCEP_DICT["NO_TABLE"]
81+
82+
if not isinstance(self.return_fields, list):
83+
return EXCEP_DICT["BAD_FORMAT_RE_FIELDS"]
84+
85+
if not self.just_keys and self.return_fields:
86+
return EXCEP_DICT["JUST_KEYS_COMPAT"]
87+
88+
if self.field and not self.value:
89+
return EXCEP_DICT["NO_VALUE"]
90+
91+
if self.ns != DEFAULT_NAMESPACE and self.ns not in multi_asic.get_namespace_list():
92+
return EXCEP_DICT["INV_NS"] + " Choose From {}".format(multi_asic.get_namespace_list())
93+
94+
verbose_print("MatchRequest Checks Passed")
95+
96+
return ""
97+
98+
def __str__(self):
99+
str = "----------------------- \n MatchRequest: \n"
100+
if self.db:
101+
str += "db:{} , ".format(self.db)
102+
if self.file:
103+
str += "file:{} , ".format(self.file)
104+
if self.table:
105+
str += "table:{} , ".format(self.table)
106+
if self.key_pattern:
107+
str += "key_pattern:{} , ".format(self.key_pattern)
108+
if self.field:
109+
str += "field:{} , ".format(self.field)
110+
if self.value:
111+
str += "value:{} , ".format(self.value)
112+
if self.just_keys:
113+
str += "just_keys:True ,"
114+
else:
115+
str += "just_keys:False ,"
116+
if len(self.return_fields) > 0:
117+
str += "return_fields: " + ",".join(self.return_fields) + " "
118+
if self.ns:
119+
str += "namespace: , " + self.ns
120+
if self.match_entire_list:
121+
str += "match_list: True , "
122+
else:
123+
str += "match_list: False , "
124+
return str
125+
126+
class SourceAdapter(ABC):
127+
""" Source Adaptor offers unified interface to Data Sources """
128+
129+
def __init__(self):
130+
pass
131+
132+
@abstractmethod
133+
def connect(self, db, ns):
134+
""" Return True for Success, False for failure """
135+
return False
136+
137+
@abstractmethod
138+
def getKeys(self, db, table, key_pattern):
139+
return []
140+
141+
@abstractmethod
142+
def get(self, db, key):
143+
return {}
144+
145+
@abstractmethod
146+
def hget(self, db, key, field):
147+
return ""
148+
149+
@abstractmethod
150+
def get_separator(self, db):
151+
return ""
152+
153+
class RedisSource(SourceAdapter):
154+
""" Concrete Adaptor Class for connecting to Redis Data Sources """
155+
156+
def __init__(self):
157+
self.conn = None
158+
159+
def connect(self, db, ns):
160+
try:
161+
if not SonicDBConfig.isInit():
162+
if multi_asic.is_multi_asic():
163+
SonicDBConfig.load_sonic_global_db_config()
164+
else:
165+
SonicDBConfig.load_sonic_db_config()
166+
self.conn = SonicV2Connector(namespace=ns, use_unix_socket_path=True)
167+
self.conn.connect(db)
168+
except Exception as e:
169+
verbose_print("RedisSource: Connection Failed\n" + str(e))
170+
return False
171+
return True
172+
173+
def get_separator(self, db):
174+
return self.conn.get_db_separator(db)
175+
176+
def getKeys(self, db, table, key_pattern):
177+
return self.conn.keys(db, table + self.get_separator(db) + key_pattern)
178+
179+
def get(self, db, key):
180+
return self.conn.get_all(db, key)
181+
182+
def hget(self, db, key, field):
183+
return self.conn.get(db, key, field)
184+
185+
class JsonSource(SourceAdapter):
186+
""" Concrete Adaptor Class for connecting to JSON Data Sources """
187+
188+
def __init__(self):
189+
self.json_data = None
190+
191+
def connect(self, db, ns):
192+
try:
193+
with open(db) as f:
194+
self.json_data = json.load(f)
195+
except Exception as e:
196+
verbose_print("JsonSource: Loading the JSON file failed" + str(e))
197+
return False
198+
return True
199+
200+
def get_separator(self, db):
201+
return SonicDBConfig.getSeparator("CONFIG_DB")
202+
203+
def getKeys(self, db, table, key_pattern):
204+
if table not in self.json_data:
205+
return []
206+
# https://docs.python.org/3.7/library/fnmatch.html
207+
kp = key_pattern.replace("[^", "[!")
208+
kys = fnmatch.filter(self.json_data[table].keys(), kp)
209+
return [table + self.get_separator(db) + ky for ky in kys]
210+
211+
def get(self, db, key):
212+
sep = self.get_separator(db)
213+
table, key = key.split(sep, 1)
214+
return self.json_data.get(table, {}).get(key, {})
215+
216+
def hget(self, db, key, field):
217+
sep = self.get_separator(db)
218+
table, key = key.split(sep, 1)
219+
return self.json_data.get(table, "").get(key, "").get(field, "")
220+
221+
class MatchEngine:
222+
""" Pass in a MatchRequest, to fetch the Matched dump from the Data sources """
223+
224+
def __get_source_adapter(self, req):
225+
src = None
226+
d_src = ""
227+
if req.db:
228+
d_src = req.db
229+
src = RedisSource()
230+
else:
231+
d_src = req.file
232+
src = JsonSource()
233+
return d_src, src
234+
235+
def __create_template(self):
236+
return {"error" : "", "keys" : [], "return_values" : {}}
237+
238+
def __display_error(self, err):
239+
template = self.__create_template()
240+
template['error'] = err
241+
verbose_print("MatchEngine: \n" + template['error'])
242+
return template
243+
244+
def __filter_out_keys(self, src, req, all_matched_keys):
245+
# TODO: Custom Callbacks for Complex Matching Criteria
246+
if not req.field:
247+
return all_matched_keys
248+
249+
filtered_keys = []
250+
for key in all_matched_keys:
251+
f_values = src.hget(req.db, key, req.field)
252+
if "," in f_values and not req.match_entire_list:
253+
f_value = f_values.split(",")
254+
else:
255+
f_value = [f_values]
256+
if req.value in f_value:
257+
filtered_keys.append(key)
258+
return filtered_keys
259+
260+
def __fill_template(self, src, req, filtered_keys, template):
261+
for key in filtered_keys:
262+
temp = {}
263+
if not req.just_keys:
264+
temp[key] = src.get(req.db, key)
265+
template["keys"].append(temp)
266+
elif len(req.return_fields) > 0:
267+
template["keys"].append(key)
268+
template["return_values"][key] = {}
269+
for field in req.return_fields:
270+
template["return_values"][key][field] = src.hget(req.db, key, field)
271+
else:
272+
template["keys"].append(key)
273+
verbose_print("Return Values:" + str(template["return_values"]))
274+
return template
275+
276+
def fetch(self, req):
277+
""" Given a request obj, find its match in the data source provided """
278+
if not isinstance(req, MatchRequest):
279+
return self.__display_error(EXCEP_DICT["INV_REQ"])
280+
281+
verbose_print(str(req))
282+
283+
if not req.key_pattern:
284+
return self.__display_error(EXCEP_DICT["NO_KEY"])
285+
286+
d_src, src = self.__get_source_adapter(req)
287+
if not src.connect(d_src, req.ns):
288+
return self.__display_error(EXCEP_DICT["CONN_ERR"])
289+
290+
template = self.__create_template()
291+
all_matched_keys = src.getKeys(req.db, req.table, req.key_pattern)
292+
if not all_matched_keys:
293+
return self.__display_error(EXCEP_DICT["NO_MATCHES"])
294+
295+
filtered_keys = self.__filter_out_keys(src, req, all_matched_keys)
296+
verbose_print("Filtered Keys:" + str(filtered_keys))
297+
if not filtered_keys:
298+
return self.__display_error(EXCEP_DICT["NO_ENTRIES"])
299+
return self.__fill_template(src, req, filtered_keys, template)
300+

0 commit comments

Comments
 (0)