Skip to content

Commit 9799471

Browse files
authored
[dvs] Refactor ACL tests to use polling interface to access redis (#1213)
* Introduce new DVSDatabase abstraction and SONiC redis fixtures * Refactor ACL tests to use redis fixtures Signed-off-by: Danny Allen <[email protected]>
1 parent 060a44e commit 9799471

File tree

4 files changed

+839
-1238
lines changed

4 files changed

+839
-1238
lines changed

tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@
1010
import tarfile
1111
import StringIO
1212
import subprocess
13+
1314
from datetime import datetime
1415
from swsscommon import swsscommon
1516

17+
pytest_plugins = [
18+
"dvslib.dvs_database"
19+
]
20+
1621
def ensure_system(cmd):
1722
(rc, output) = commands.getstatusoutput(cmd)
1823
if rc:

tests/dvslib/__init__.py

Whitespace-only changes.

tests/dvslib/dvs_database.py

Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
"""
2+
dvs_database contains utilities for interacting with redis when writing
3+
tests for the virtual switch.
4+
"""
5+
from __future__ import print_function
6+
7+
import time
8+
import collections
9+
import pytest
10+
11+
from swsscommon import swsscommon
12+
13+
APP_DB_ID = 0
14+
ASIC_DB_ID = 1
15+
COUNTERS_DB_ID = 2
16+
CONFIG_DB_ID = 4
17+
FLEX_COUNTER_DB_ID = 5
18+
STATE_DB_ID = 6
19+
20+
21+
# PollingConfig provides parameters that are used to control polling behavior
22+
# when accessing redis:
23+
# - polling_interval: how often to check for updates in redis
24+
# - timeout: the max amount of time to wait for updates in redis
25+
# - strict: if the strict flag is set, failure to receive updates will cause
26+
# the polling method to cause tests to fail (e.g. assert False)
27+
PollingConfig = collections.namedtuple('PollingConfig', 'polling_interval timeout strict')
28+
29+
30+
class DVSDatabase(object):
31+
"""
32+
DVSDatabase provides access to redis databases on the virtual switch.
33+
34+
By default, database operations are configured to use
35+
`DEFAULT_POLLING_CONFIG`. Users can specify their own PollingConfig,
36+
but this shouldn't typically be necessary.
37+
"""
38+
DEFAULT_POLLING_CONFIG = PollingConfig(polling_interval=0.01, timeout=5, strict=True)
39+
40+
def __init__(self, db_id, connector):
41+
"""
42+
Initializes a DVSDatabase instance.
43+
44+
Args:
45+
db_id (int): The integer ID used to identify the given database
46+
instance in redis.
47+
connector (str): The I/O connection used to communicate with
48+
redis (e.g. unix socket, tcp socket, etc.).
49+
50+
NOTE: Currently it's most convenient to let the user specify the
51+
connector since it's set up in the dvs fixture. We may abstract
52+
this further in the future as we refactor dvs.
53+
"""
54+
55+
self.db_connection = swsscommon.DBConnector(db_id, connector, 0)
56+
57+
def create_entry(self, table_name, key, entry):
58+
"""
59+
Adds the mapping {`key` -> `entry`} to the specified table.
60+
61+
Args:
62+
table_name (str): The name of the table to add the entry to.
63+
key (str): The key that maps to the entry.
64+
entry (Dict[str, str]): A set of key-value pairs to be stored.
65+
"""
66+
67+
table = swsscommon.Table(self.db_connection, table_name)
68+
formatted_entry = swsscommon.FieldValuePairs(entry.items())
69+
table.set(key, formatted_entry)
70+
71+
def wait_for_entry(self, table_name, key,
72+
polling_config=DEFAULT_POLLING_CONFIG):
73+
"""
74+
Gets the entry stored at `key` in the specified table. This method
75+
will wait for the entry to exist.
76+
77+
Args:
78+
table_name (str): The name of the table where the entry is
79+
stored.
80+
key (str): The key that maps to the entry being retrieved.
81+
polling_config (PollingConfig): The parameters to use to poll
82+
the db.
83+
84+
Returns:
85+
Dict[str, str]: The entry stored at `key`. If no entry is found,
86+
then an empty Dict will be returned.
87+
88+
"""
89+
90+
access_function = self._get_entry_access_function(table_name, key, True)
91+
return self._db_poll(polling_config, access_function)
92+
93+
def delete_entry(self, table_name, key):
94+
"""
95+
Removes the entry stored at `key` in the specified table.
96+
97+
Args:
98+
table_name (str): The name of the table where the entry is
99+
being removed.
100+
key (str): The key that maps to the entry being removed.
101+
"""
102+
103+
table = swsscommon.Table(self.db_connection, table_name)
104+
table._del(key) # pylint: disable=protected-access
105+
106+
def wait_for_empty_entry(self,
107+
table_name,
108+
key,
109+
polling_config=DEFAULT_POLLING_CONFIG):
110+
"""
111+
Checks if there is any entry stored at `key` in the specified
112+
table. This method will wait for the entry to be empty.
113+
114+
Args:
115+
table_name (str): The name of the table being checked.
116+
key (str): The key to be checked.
117+
polling_config (PollingConfig): The parameters to use to poll
118+
the db.
119+
120+
Returns:
121+
bool: True if no entry exists at `key`, False otherwise.
122+
"""
123+
124+
access_function = self._get_entry_access_function(table_name, key, False)
125+
return not self._db_poll(polling_config, access_function)
126+
127+
def wait_for_n_keys(self,
128+
table_name,
129+
num_keys,
130+
polling_config=DEFAULT_POLLING_CONFIG):
131+
"""
132+
Gets all of the keys stored in the specified table. This method
133+
will wait for the specified number of keys.
134+
135+
Args:
136+
table_name (str): The name of the table from which to fetch
137+
the keys.
138+
num_keys (int): The expected number of keys to retrieve from
139+
the table.
140+
polling_config (PollingConfig): The parameters to use to poll
141+
the db.
142+
143+
Returns:
144+
List[str]: The keys stored in the table. If no keys are found,
145+
then an empty List will be returned.
146+
"""
147+
148+
access_function = self._get_keys_access_function(table_name, num_keys)
149+
return self._db_poll(polling_config, access_function)
150+
151+
def _get_keys_access_function(self, table_name, num_keys):
152+
"""
153+
Generates an access function to check for `num_keys` in the given
154+
table and return the list of keys if successful.
155+
156+
Args:
157+
table_name (str): The name of the table from which to fetch
158+
the keys.
159+
num_keys (int): The number of keys to check for in the table.
160+
If this is set to None, then this function will just return
161+
whatever keys are in the table.
162+
163+
Returns:
164+
Callable([[], (bool, List[str])]): A function that can be
165+
called to access the database.
166+
167+
If `num_keys` keys are found in the given table, or left
168+
unspecified, then the function will return True along with
169+
the list of keys that were found. Otherwise, the function will
170+
return False and some undefined list of keys.
171+
"""
172+
173+
table = swsscommon.Table(self.db_connection, table_name)
174+
175+
def _accessor():
176+
keys = table.getKeys()
177+
if not keys:
178+
keys = []
179+
180+
if not num_keys and num_keys != 0:
181+
status = True
182+
else:
183+
status = len(keys) == num_keys
184+
185+
return (status, keys)
186+
187+
return _accessor
188+
189+
def _get_entry_access_function(self, table_name, key, expect_entry):
190+
"""
191+
Generates an access function to check for existence of an entry
192+
at `key` and return it if successful.
193+
194+
Args:
195+
table_name (str): The name of the table from which to fetch
196+
the entry.
197+
key (str): The key that maps to the entry being retrieved.
198+
expect_entry (bool): Whether or not we expect to see an entry
199+
at `key`.
200+
201+
Returns:
202+
Callable([[], (bool, Dict[str, str])]): A function that can be
203+
called to access the database.
204+
205+
If `expect_entry` is set and an entry is found, then the
206+
function will return True along with the entry that was found.
207+
208+
If `expect_entry` is not set and no entry is found, then the
209+
function will return True along with an empty Dict.
210+
211+
In all other cases, the function will return False with some
212+
undefined Dict.
213+
"""
214+
215+
table = swsscommon.Table(self.db_connection, table_name)
216+
217+
def _accessor():
218+
(status, fv_pairs) = table.get(key)
219+
220+
status = expect_entry == status
221+
222+
if fv_pairs:
223+
entry = dict(fv_pairs)
224+
else:
225+
entry = {}
226+
227+
return (status, entry)
228+
229+
return _accessor
230+
231+
@staticmethod
232+
def _db_poll(polling_config, access_function):
233+
"""
234+
_db_poll will periodically run `access_function` on the database
235+
using the parameters described in `polling_config` and return the
236+
output of the access function.
237+
238+
Args:
239+
polling_config (PollingConfig): The parameters to use to poll
240+
the db.
241+
access_function (Callable[[], (bool, Any)]): The function used
242+
for polling the db. Note that the function must return a
243+
status which indicates if the function was succesful or
244+
not, as well as some return value.
245+
246+
Returns:
247+
Any: The output of the access function, if it is succesful,
248+
None otherwise.
249+
"""
250+
if polling_config.polling_interval == 0:
251+
iterations = 1
252+
else:
253+
iterations = int(polling_config.timeout // polling_config.polling_interval) + 1
254+
255+
for _ in range(iterations):
256+
(status, result) = access_function()
257+
258+
if status:
259+
return result
260+
261+
time.sleep(polling_config.polling_interval)
262+
263+
if polling_config.strict:
264+
assert False
265+
266+
return None
267+
268+
269+
@pytest.fixture
270+
def app_db(dvs):
271+
"""
272+
Provides access to the SONiC APP DB.
273+
274+
Args:
275+
dvs (DockerVirtualSwitch): The dvs fixture, automatically injected
276+
by pytest.
277+
278+
Returns:
279+
DVSDatabase: An instance of APP DB
280+
"""
281+
282+
return DVSDatabase(APP_DB_ID, dvs.redis_sock)
283+
284+
285+
@pytest.fixture
286+
def asic_db(dvs):
287+
"""
288+
Provides access to the SONiC ASIC DB.
289+
290+
Args:
291+
dvs (DockerVirtualSwitch): The dvs fixture, automatically injected
292+
by pytest.
293+
294+
Attributes:
295+
default_acl_tables (List[str]): IDs for the ACL tables that are
296+
configured in SONiC by default
297+
default_acl_entries (List[str]): IDs for the ACL rules that are
298+
configured in SONiC by default
299+
port_name_map (Dict[str, str]): A mapping from interface names
300+
(e.g. Ethernet0) to port IDs
301+
302+
Returns:
303+
DVSDatabase: An instance of ASIC DB
304+
"""
305+
306+
db = DVSDatabase(ASIC_DB_ID, dvs.redis_sock) # pylint: disable=invalid-name
307+
308+
# NOTE: This is an ugly hack to emulate the current asic db behavior,
309+
# this will be refactored along with the dvs fixture.
310+
db.default_acl_tables = dvs.asicdb.default_acl_tables # pylint: disable=attribute-defined-outside-init
311+
db.default_acl_entries = dvs.asicdb.default_acl_entries # pylint: disable=attribute-defined-outside-init
312+
db.port_name_map = dvs.asicdb.portnamemap # pylint: disable=attribute-defined-outside-init
313+
314+
return db
315+
316+
317+
@pytest.fixture
318+
def counters_db(dvs):
319+
"""
320+
Provides access to the SONiC Counters DB.
321+
322+
Args:
323+
dvs (DockerVirtualSwitch): The dvs fixture, automatically injected
324+
by pytest.
325+
326+
Returns:
327+
DVSDatabase: An instance of Counters DB
328+
"""
329+
330+
return DVSDatabase(COUNTERS_DB_ID, dvs.redis_sock)
331+
332+
333+
@pytest.fixture
334+
def config_db(dvs):
335+
"""
336+
Provides access to the SONiC Config DB.
337+
338+
Args:
339+
dvs (DockerVirtualSwitch): The dvs fixture, automatically injected
340+
by pytest.
341+
342+
Returns:
343+
DVSDatabase: An instance of Config DB
344+
"""
345+
346+
return DVSDatabase(CONFIG_DB_ID, dvs.redis_sock)
347+
348+
349+
@pytest.fixture
350+
def flex_counter_db(dvs):
351+
"""
352+
Provides access to the SONiC Flex Counter DB.
353+
354+
Args:
355+
dvs (DockerVirtualSwitch): The dvs fixture, automatically injected
356+
by pytest.
357+
358+
Returns:
359+
DVSDatabase: An instance of Flex Counter DB
360+
"""
361+
362+
return DVSDatabase(FLEX_COUNTER_DB_ID, dvs.redis_sock)
363+
364+
365+
@pytest.fixture
366+
def state_db(dvs):
367+
"""
368+
Provides access to the SONiC State DB.
369+
370+
Args:
371+
dvs (DockerVirtualSwitch): The dvs fixture, automatically injected
372+
by pytest.
373+
374+
Returns:
375+
DVSDatabase: An instance of State DB
376+
"""
377+
378+
return DVSDatabase(STATE_DB_ID, dvs.redis_sock)

0 commit comments

Comments
 (0)