Skip to content

Commit

Permalink
python: Make key-value matching strict by default.
Browse files Browse the repository at this point in the history
Currently, if a key is not found in the decoder information, we use the
default decoder which typically returns a string.

This not only means we can go out of sync with the C code without
noticing but it's also error prone as malformed flows could be parsed
without warning.

Make KeyValue parsing strict, raising an error if a decoder is not found
for a key.
This behaviour can be turned off globally by running 'KVDecoders.strict
= False' but it's generally not recommended. Also, if a KVDecoder does
need this default behavior, it can be explicitly configured specifying
it's default decoder.

Signed-off-by: Adrian Moreno <[email protected]>
Acked-by: Mike Pattrick <[email protected]>
Signed-off-by: Ilya Maximets <[email protected]>
Signed-off-by: Simon Horman <[email protected]>
  • Loading branch information
amorenoz authored and Simon Horman committed Jan 18, 2024
1 parent 0a99a08 commit 12a3789
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 19 deletions.
25 changes: 18 additions & 7 deletions python/ovs/flow/kv.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,23 +85,29 @@ class KVDecoders(object):
reason, the default_free decoder, must return both the key and value to be
stored.
Globally defined "strict" variable controls what to do when decoders do not
contain a valid decoder for a key and a default function is not provided.
If set to True (default), a ParseError is raised.
If set to False, the value will be decoded as a string.
Args:
decoders (dict): Optional; A dictionary of decoders indexed by keyword.
default (callable): Optional; A function to use if a match is not
found in configured decoders. If not provided, the default behavior
is to try to decode the value into an integer and, if that fails,
just return the string as-is. The function must accept a the key
and the value and return the decoded (key, value) tuple back.
depends on "strict". The function must accept a the key and a value
and return the decoded (key, value) tuple back.
default_free (callable): Optional; The decoder used if a match is not
found in configured decoders and it's a free value (e.g:
a value without a key) Defaults to returning the free value as
keyword and "True" as value.
The callable must accept a string and return a key-value pair.
"""

strict = True

def __init__(self, decoders=None, default=None, default_free=None):
self._decoders = decoders or dict()
self._default = default or (lambda k, v: (k, decode_default(v)))
self._default = default
self._default_free = default_free or self._default_free_decoder

def decode(self, keyword, value_str):
Expand All @@ -127,9 +133,14 @@ def decode(self, keyword, value_str):
return keyword, value
else:
if value_str:
return self._default(keyword, value_str)
else:
return self._default_free(keyword)
if self._default:
return self._default(keyword, value_str)
if self.strict:
raise ParseError(
"Cannot parse key {}: No decoder found".format(keyword)
)
return keyword, decode_default(value_str)
return self._default_free(keyword)

@staticmethod
def _default_free_decoder(key):
Expand Down
7 changes: 6 additions & 1 deletion python/ovs/flow/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ def decode(self, index, value_str):
value_str (str): The value string to decode.
"""
if index < 0 or index >= len(self._decoders):
return self._default_decoder(index, value_str)
if self._default_decoder:
return self._default_decoder(index, value_str)
else:
raise ParseError(
f"Cannot decode element {index} in list: {value_str}"
)

try:
key = self._decoders[index][0]
Expand Down
20 changes: 10 additions & 10 deletions python/ovs/tests/test_kv.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import pytest

from ovs.flow.kv import KVParser, KeyValue
from ovs.flow.kv import KVParser, KVDecoders, KeyValue
from ovs.flow.decoders import decode_default

decoders = KVDecoders(default=lambda k, v: (k, decode_default(v)))


@pytest.mark.parametrize(
Expand All @@ -9,7 +12,7 @@
(
(
"cookie=0x0, duration=147566.365s, table=0, n_packets=39, n_bytes=2574, idle_age=65534, hard_age=65534", # noqa: E501
None,
decoders,
),
[
KeyValue("cookie", 0),
Expand All @@ -24,7 +27,7 @@
(
(
"load:0x4->NXM_NX_REG13[],load:0x9->NXM_NX_REG11[],load:0x8->NXM_NX_REG12[],load:0x1->OXM_OF_METADATA[],load:0x1->NXM_NX_REG14[],mod_dl_src:0a:58:a9:fe:00:02,resubmit(,8)", # noqa: E501
None,
decoders,
),
[
KeyValue("load", "0x4->NXM_NX_REG13[]"),
Expand All @@ -36,20 +39,17 @@
KeyValue("resubmit", ",8"),
],
),
(("l1(l2(l3(l4())))", decoders), [KeyValue("l1", "l2(l3(l4()))")]),
(
("l1(l2(l3(l4())))", None),
[KeyValue("l1", "l2(l3(l4()))")]
),
(
("l1(l2(l3(l4()))),foo:bar", None),
("l1(l2(l3(l4()))),foo:bar", decoders),
[KeyValue("l1", "l2(l3(l4()))"), KeyValue("foo", "bar")],
),
(
("enqueue:1:2,output=2", None),
("enqueue:1:2,output=2", decoders),
[KeyValue("enqueue", "1:2"), KeyValue("output", 2)],
),
(
("value_to_reg(100)->someReg[10],foo:bar", None),
("value_to_reg(100)->someReg[10],foo:bar", decoders),
[
KeyValue("value_to_reg", "(100)->someReg[10]"),
KeyValue("foo", "bar"),
Expand Down
28 changes: 27 additions & 1 deletion python/ovs/tests/test_ofp.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import pytest

from ovs.flow.ofp import OFPFlow
from ovs.flow.kv import KeyValue
from ovs.flow.kv import KeyValue, ParseError
from ovs.flow.decoders import EthMask, IPMask, decode_mask


Expand Down Expand Up @@ -509,11 +509,37 @@
),
],
),
(
"actions=doesnotexist(1234)",
ParseError,
),
(
"actions=learn(eth_type=nofield)",
ParseError,
),
(
"actions=learn(nofield=eth_type)",
ParseError,
),
(
"nofield=0x123 actions=drop",
ParseError,
),
(
"actions=load:0x12334->NOFILED",
ParseError,
),
],
)
def test_act(input_string, expected):
if isinstance(expected, type):
with pytest.raises(expected):
ofp = OFPFlow(input_string)
return

ofp = OFPFlow(input_string)
actions = ofp.actions_kv

for i in range(len(expected)):
assert expected[i].key == actions[i].key
assert expected[i].value == actions[i].value
Expand Down

0 comments on commit 12a3789

Please sign in to comment.