Trait Based Networking Filter Expression Parsing and Base Models
Base models for the majority of the Trait Based Networking feature. Adds a lark-based parser for filter expressions found in Trait Based Networking configuration files. Change-Id: I4414463c70d37a7c6b5a957941a2607b5c15ab9e Signed-off-by: Clif Houck <me@clifhouck.com>
This commit is contained in:
@@ -1131,3 +1131,19 @@ class ConfigDriveRegenerationFailure(IronicException):
|
||||
"""Raised when we fail to handle configuration drive corrections."""
|
||||
# NOTE(TheJulia): This is not intended to get raised to a user, but more
|
||||
# so we handle known possible failure cases and don't fail horribly.
|
||||
|
||||
|
||||
class TraitBasedNetworkingException(IronicException):
|
||||
"""Raised when there's an issue with trait based networking feature."""
|
||||
pass
|
||||
|
||||
|
||||
class TBNComparatorPrefixMatchTypeMismatch(TraitBasedNetworkingException):
|
||||
"""Comparator prefix match can only be used against a string."""
|
||||
pass
|
||||
|
||||
|
||||
class TBNAttributeRetrievalException(TraitBasedNetworkingException):
|
||||
"""Specified attribute could not be found."""
|
||||
_msg_fmt = _("Could not retrieve attribute %(attr_name)s from "
|
||||
"passed object")
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import enum
|
||||
|
||||
import ironic.common.exception as exc
|
||||
from ironic.common.i18n import _
|
||||
import ironic.common.trait_based_networking.grammar.parser as tbn_parser
|
||||
|
||||
|
||||
class Operator(enum.Enum):
|
||||
AND = "&&"
|
||||
OR = "||"
|
||||
|
||||
def eval(self, variable, value):
|
||||
# NOTE(clif): These can operate on string values, and return the values
|
||||
# themselves instead of a boolean!
|
||||
match self.name:
|
||||
case self.AND.name:
|
||||
return variable and value
|
||||
case self.OR.name:
|
||||
return variable or value
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class Comparator(enum.Enum):
|
||||
EQUALITY = "=="
|
||||
INEQUALITY = "!="
|
||||
GT_OR_EQ = ">="
|
||||
GT = ">"
|
||||
LT_OR_EQ = "<="
|
||||
LT = "<"
|
||||
PREFIX_MATCH = "=~"
|
||||
|
||||
def eval(self, variable, value):
|
||||
# TODO(clif): Should we some sort of checking of variable type vs
|
||||
# requested operator?
|
||||
match self.name:
|
||||
case self.EQUALITY.name:
|
||||
return variable == value
|
||||
case self.INEQUALITY.name:
|
||||
return variable != value
|
||||
case self.GT_OR_EQ.name:
|
||||
return variable >= value
|
||||
case self.GT.name:
|
||||
return variable > value
|
||||
case self.LT_OR_EQ.name:
|
||||
return variable <= value
|
||||
case self.LT.name:
|
||||
return variable < value
|
||||
case self.PREFIX_MATCH.name:
|
||||
if isinstance(variable, str):
|
||||
return variable.startswith(value)
|
||||
raise exc.TBNComparatorPrefixMatchTypeMismatch(
|
||||
_("Prefix match can only be used with variables "
|
||||
"of type string")
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class Actions(enum.Enum):
|
||||
ATTACH_PORT = "attach_port"
|
||||
ATTACH_PORTGROUP = "attach_portgroup"
|
||||
BOND_PORTS = "bond_ports"
|
||||
GROUP_AND_ATTACH_PORTS = "group_and_attach_ports"
|
||||
|
||||
|
||||
class Variables(enum.Enum):
|
||||
NETWORK_NAME = "network.name"
|
||||
NETWORK_TAGS = "network.tags"
|
||||
|
||||
PORT_ADDRESS = "port.address"
|
||||
PORT_CATEGORY = "port.category"
|
||||
PORT_IS_PORT = "port.is_port"
|
||||
PORT_IS_PORTGROUP = "port.is_portgroup"
|
||||
PORT_PHYSICAL_NETWORK = "port.physical_network"
|
||||
PORT_VENDOR = "port.vendor"
|
||||
|
||||
def object_name(self):
|
||||
return str(self).split(".")[0]
|
||||
|
||||
def attribute_name(self):
|
||||
return str(self).split(".")[1]
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
def retrieve_attribute(attribute_name, tbn_obj):
|
||||
attribute = getattr(tbn_obj, attribute_name, None)
|
||||
|
||||
if attribute is None:
|
||||
raise exc.TBNAttributeRetrievalException(attr_name=attribute_name)
|
||||
|
||||
return attribute
|
||||
|
||||
|
||||
class FunctionExpression(object):
|
||||
def __init__(self, variable):
|
||||
self._variable = variable
|
||||
|
||||
def eval(self, port, network):
|
||||
tbn_obj = port if self._variable.object_name() == "port" else network
|
||||
attr_name = self._variable.attribute_name()
|
||||
attr_func = retrieve_attribute(attr_name, tbn_obj)
|
||||
return attr_func()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self._variable}"
|
||||
|
||||
|
||||
class SingleExpression(object):
|
||||
def __init__(self, variable, comparator, literal):
|
||||
self._variable = variable
|
||||
self._comparator = comparator
|
||||
self._literal = literal
|
||||
|
||||
def eval(self, port, network):
|
||||
tbn_obj = port if self._variable.object_name() == "port" else network
|
||||
attr_name = self._variable.attribute_name()
|
||||
attribute = retrieve_attribute(attr_name, tbn_obj)
|
||||
return self._comparator.eval(attribute, self._literal)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self._variable} {self._comparator} '{self._literal}'"
|
||||
|
||||
|
||||
class CompoundExpression(object):
|
||||
def __init__(self, left_expression, operator, right_expression):
|
||||
self._left_expression = left_expression
|
||||
self._operator = operator
|
||||
self._right_expression = right_expression
|
||||
|
||||
def eval(self, port, network):
|
||||
left_result = self._left_expression.eval(port, network)
|
||||
right_result = self._right_expression.eval(port, network)
|
||||
match self._operator:
|
||||
case Operator.OR:
|
||||
return left_result or right_result
|
||||
case Operator.AND:
|
||||
return left_result and right_result
|
||||
|
||||
def __str__(self):
|
||||
return (f"{self._left_expression} {self._operator} "
|
||||
f"{self._right_expression}")
|
||||
|
||||
|
||||
class ParenExpression(object):
|
||||
def __init__(self, expression):
|
||||
self._expression = expression
|
||||
|
||||
def eval(self, port, network):
|
||||
return self._expression.eval(port, network)
|
||||
|
||||
def __str__(self):
|
||||
return f"({self._expression})"
|
||||
|
||||
|
||||
class FilterExpression(object):
|
||||
def __init__(self, expression):
|
||||
self._expression = expression
|
||||
|
||||
def eval(self, port, network):
|
||||
return self._expression.eval(port, network)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self._expression}"
|
||||
|
||||
@classmethod
|
||||
def parse(cls, expression):
|
||||
tree = tbn_parser.FilterExpressionParser.parse(expression)
|
||||
return tbn_parser.FilterExpressionTransformer().transform(tree)
|
||||
|
||||
def __eq__(self, other):
|
||||
return str(self) == str(other)
|
||||
|
||||
|
||||
class TraitAction(object):
|
||||
NECESSARY_KEYS = [
|
||||
'action',
|
||||
'filter',
|
||||
]
|
||||
OPTIONAL_KEYS = [
|
||||
'max_count',
|
||||
'min_count',
|
||||
]
|
||||
ALL_KEYS = OPTIONAL_KEYS + NECESSARY_KEYS
|
||||
|
||||
def __init__(self, trait_name, action, filter_expression,
|
||||
min_count=None, max_count=None):
|
||||
self.trait_name = trait_name
|
||||
self.action = action
|
||||
self.filter_expression = filter_expression
|
||||
self.min_count = min_count
|
||||
self.max_count = max_count
|
||||
|
||||
def matches(self, portlike, network):
|
||||
"""Check if filter expression matches the port, network pairing."""
|
||||
return self.filter_expression.eval(portlike, network)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.trait_name == other.trait_name
|
||||
and self.action == other.action
|
||||
and self.filter_expression == other.filter_expression
|
||||
and self.min_count == other.min_count
|
||||
and self.max_count == other.max_count)
|
||||
|
||||
|
||||
class NetworkTrait(object):
|
||||
def __init__(self, name, actions):
|
||||
self.name = name
|
||||
self.actions = actions
|
||||
|
||||
def __eq__(self, other):
|
||||
if self.name != other.name:
|
||||
return False
|
||||
|
||||
for action in self.actions:
|
||||
match_found = False
|
||||
for other_action in other.actions:
|
||||
if action == other_action:
|
||||
match_found = True
|
||||
break
|
||||
|
||||
if not match_found:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class PrimordialPort(object):
|
||||
def __init__(self, ironic_port_like_obj):
|
||||
# NOTE(clif): Both ironic port and portgroups should support the
|
||||
# attributes below.
|
||||
self.id = ironic_port_like_obj.id
|
||||
self.uuid = ironic_port_like_obj.uuid
|
||||
self.address = ironic_port_like_obj.address
|
||||
self.category = ironic_port_like_obj.category
|
||||
self.physical_network = ironic_port_like_obj.physical_network
|
||||
self.vendor = ironic_port_like_obj.vendor
|
||||
|
||||
|
||||
class Port(PrimordialPort):
|
||||
@classmethod
|
||||
def from_ironic_port(cls, ironic_port):
|
||||
return Port(ironic_port)
|
||||
|
||||
def is_port(self):
|
||||
return True
|
||||
|
||||
def is_portgroup(self):
|
||||
return False
|
||||
|
||||
|
||||
class Portgroup(PrimordialPort):
|
||||
@classmethod
|
||||
def from_ironic_portgroup(cls, ironic_portgroup):
|
||||
return Portgroup(ironic_portgroup)
|
||||
|
||||
def is_port(self):
|
||||
return False
|
||||
|
||||
def is_portgroup(self):
|
||||
return True
|
||||
|
||||
|
||||
# TODO(Clif): Draw from VIFs
|
||||
class Network(object):
|
||||
def __init__(self, network_id, name, tags):
|
||||
self.id = network_id
|
||||
self.name = name
|
||||
self.tags = tags
|
||||
|
||||
|
||||
class RenderedAction(object):
|
||||
def __init__(self, trait_action, node_uuid):
|
||||
self._trait_action = trait_action
|
||||
self._node_uuid = node_uuid
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self._trait_action == other._trait_action
|
||||
and self._node_uuid == other._node_uuid)
|
||||
|
||||
|
||||
class AttachPort(RenderedAction):
|
||||
def __init__(self, trait_action, node_uuid, port_uuid, network_id):
|
||||
super().__init__(trait_action, node_uuid)
|
||||
self._port_uuid = port_uuid
|
||||
self._network_id = network_id
|
||||
|
||||
def __str__(self):
|
||||
return _(f"Attach port '{self._port_uuid}' on node "
|
||||
f"'{self._node_uuid}' to network '{self._network_id}' "
|
||||
f"via trait {self._trait_action.trait_name}")
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, AttachPort)
|
||||
and self._port_uuid == other._port_uuid
|
||||
and self._network_id == other._network_id
|
||||
and super().__eq__(other))
|
||||
|
||||
|
||||
class AttachPortgroup(RenderedAction):
|
||||
def __init__(self, trait_action, node_uuid, portgroup_uuid, network_id):
|
||||
super().__init__(trait_action, node_uuid)
|
||||
self._portgroup_uuid = portgroup_uuid
|
||||
self._network_id = network_id
|
||||
|
||||
def __str__(self):
|
||||
return _(f"Attach portgroup '{self._portgroup_uuid}' on node "
|
||||
f"'{self._node_uuid}' to network '{self._network_id}'"
|
||||
f"via trait {self._trait_action.trait_name}")
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, AttachPortgroup)
|
||||
and self._portgroup_uuid == other._portgroup_uuid
|
||||
and self._network_id == other._network_id
|
||||
and super().__eq__(other))
|
||||
|
||||
|
||||
class NoMatch(RenderedAction):
|
||||
def __init__(self, trait_action, node_uuid, reason):
|
||||
super().__init__(trait_action, node_uuid)
|
||||
self._reason = reason
|
||||
|
||||
def __str__(self):
|
||||
return _(f"No match found for action under trait "
|
||||
f"'{self._trait_action.trait_name}' "
|
||||
f"on node '{self._node_uuid}': {self._reason}")
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, NoMatch)
|
||||
and self._reason == other._reason
|
||||
and super().__eq__(other))
|
||||
|
||||
|
||||
class NotImplementedAction(RenderedAction):
|
||||
def __init__(self, action):
|
||||
self._action = action
|
||||
|
||||
def __str__(self):
|
||||
return _(f"Action '{self._action.value}' not yet implemented.")
|
||||
@@ -0,0 +1,114 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import ironic.common.trait_based_networking.base as tbn_base
|
||||
|
||||
import lark
|
||||
|
||||
FILTER_EXPRESSION_GRAMMAR = r"""
|
||||
filter_expression: single_expression
|
||||
| compound_expression
|
||||
| paren_expression
|
||||
| function_expression
|
||||
|
||||
function_expression: function
|
||||
|
||||
single_expression: variable_name comparator string_literal
|
||||
|
||||
compound_expression: filter_expression ( boolean_operator filter_expression )+
|
||||
|
||||
paren_expression: "(" filter_expression ")"
|
||||
|
||||
boolean_operator: "&&" -> op_and
|
||||
| "||" -> op_or
|
||||
|
||||
comparator: "==" -> equality
|
||||
| "!=" -> inequality
|
||||
| ">=" -> gt_or_eq
|
||||
| ">" -> gt
|
||||
| "<=" -> lt_or_eq
|
||||
| "<" -> lt
|
||||
| "=~" -> prefix_match
|
||||
|
||||
function: "is_port" -> port_is_port
|
||||
| "is_portgroup" -> port_is_portgroup
|
||||
|
||||
string_literal: /\'[A-Za-z0-9_\-\.]*\'/
|
||||
variable_name: /[a-z]+\.[a-z\_]+/
|
||||
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
"""
|
||||
|
||||
FILTER_EXPRESSION_GRAMMAR_START_RULE = 'filter_expression'
|
||||
|
||||
FilterExpressionParser = lark.Lark(FILTER_EXPRESSION_GRAMMAR,
|
||||
start=FILTER_EXPRESSION_GRAMMAR_START_RULE)
|
||||
|
||||
|
||||
class FilterExpressionTransformer(lark.Transformer):
|
||||
def op_and(self, items):
|
||||
return tbn_base.Operator.AND
|
||||
|
||||
def op_or(self, items):
|
||||
return tbn_base.Operator.OR
|
||||
|
||||
def equality(self, items):
|
||||
return tbn_base.Comparator.EQUALITY
|
||||
|
||||
def inequality(self, items):
|
||||
return tbn_base.Comparator.INEQUALITY
|
||||
|
||||
def gt_or_eq(self, items):
|
||||
return tbn_base.Comparator.GT_OR_EQ
|
||||
|
||||
def gt(self, items):
|
||||
return tbn_base.Comparator.GT
|
||||
|
||||
def lt_or_eq(self, items):
|
||||
return tbn_base.Comparator.LT_OR_EQ
|
||||
|
||||
def lt(self, items):
|
||||
return tbn_base.Comparator.LT
|
||||
|
||||
def prefix_match(self, items):
|
||||
return tbn_base.Comparator.PREFIX_MATCH
|
||||
|
||||
def single_expression(self, items):
|
||||
return tbn_base.SingleExpression(items[0], items[1], items[2])
|
||||
|
||||
def compound_expression(self, items):
|
||||
return tbn_base.CompoundExpression(items[0], items[1], items[2])
|
||||
|
||||
def paren_expression(self, items):
|
||||
return tbn_base.ParenExpression(items[0])
|
||||
|
||||
def filter_expression(self, items):
|
||||
return tbn_base.FilterExpression(items[0])
|
||||
|
||||
def port_is_port(self, items):
|
||||
return tbn_base.Variables.PORT_IS_PORT
|
||||
|
||||
def port_is_portgroup(self, items):
|
||||
return tbn_base.Variables.PORT_IS_PORTGROUP
|
||||
|
||||
def function_expression(self, items):
|
||||
return tbn_base.FunctionExpression(items[0])
|
||||
|
||||
def variable_name(self, items):
|
||||
token = items[0]
|
||||
return tbn_base.Variables(token.value)
|
||||
|
||||
def string_literal(self, items):
|
||||
token = items[0]
|
||||
# Strip ' characters from literal.
|
||||
return token.value[1:-1]
|
||||
@@ -0,0 +1,346 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import ironic.common.exception as exc
|
||||
import ironic.common.trait_based_networking.base as tbn
|
||||
|
||||
from ironic.tests import base
|
||||
import ironic.tests.unit.common.trait_based_networking.utils as tbn_test_utils
|
||||
|
||||
import itertools
|
||||
|
||||
|
||||
class TraitBasedNetworkingBaseTestCase(base.TestCase):
|
||||
def test_filter_expression_str_representation(self):
|
||||
exp = tbn.SingleExpression(
|
||||
tbn.Variables.PORT_ADDRESS, tbn.Comparator.EQUALITY, "test"
|
||||
)
|
||||
self.assertEqual("port.address == 'test'", str(exp))
|
||||
|
||||
def test_filter_object_missing_attribute_raises(self):
|
||||
exp = tbn.SingleExpression(
|
||||
tbn.Variables.PORT_ADDRESS, tbn.Comparator.EQUALITY, "test"
|
||||
)
|
||||
|
||||
obj = tbn.Port.from_ironic_port(
|
||||
tbn_test_utils.FauxPortLikeObject(address=None))
|
||||
net = tbn_test_utils.FauxNetwork()
|
||||
self.assertRaises(exc.TraitBasedNetworkingException, exp.eval,
|
||||
obj, net)
|
||||
|
||||
def test_filter_comparator_eval_equality(self):
|
||||
exp = tbn.SingleExpression(
|
||||
tbn.Variables.PORT_ADDRESS, tbn.Comparator.EQUALITY, "test"
|
||||
)
|
||||
|
||||
obj = tbn.Port.from_ironic_port(tbn_test_utils.FauxPortLikeObject())
|
||||
net = tbn_test_utils.FauxNetwork()
|
||||
self.assertTrue(exp.eval(obj, net))
|
||||
|
||||
obj = tbn.Port.from_ironic_port(
|
||||
tbn_test_utils.FauxPortLikeObject(address="bad")
|
||||
)
|
||||
self.assertFalse(exp.eval(obj, net))
|
||||
|
||||
def test_filter_comparator_eval_inequality(self):
|
||||
exp = tbn.SingleExpression(
|
||||
tbn.Variables.PORT_ADDRESS, tbn.Comparator.INEQUALITY, "test"
|
||||
)
|
||||
|
||||
obj = tbn.Port.from_ironic_port(tbn_test_utils.FauxPortLikeObject())
|
||||
net = tbn_test_utils.FauxNetwork()
|
||||
self.assertFalse(exp.eval(obj, net))
|
||||
|
||||
obj = tbn.Port.from_ironic_port(
|
||||
tbn_test_utils.FauxPortLikeObject(address="bad")
|
||||
)
|
||||
self.assertTrue(exp.eval(obj, net))
|
||||
|
||||
def test_filter_comparator_eval_greater_than_or_equal(self):
|
||||
exp = tbn.SingleExpression(
|
||||
tbn.Variables.PORT_CATEGORY, tbn.Comparator.GT_OR_EQ, "test"
|
||||
)
|
||||
|
||||
obj = tbn.Port.from_ironic_port(
|
||||
tbn_test_utils.FauxPortLikeObject(category="test2")
|
||||
)
|
||||
net = tbn_test_utils.FauxNetwork()
|
||||
self.assertTrue(exp.eval(obj, net))
|
||||
|
||||
obj = tbn.Port.from_ironic_port(
|
||||
tbn_test_utils.FauxPortLikeObject(category="test")
|
||||
)
|
||||
self.assertTrue(exp.eval(obj, net))
|
||||
|
||||
obj = tbn.Port.from_ironic_port(
|
||||
tbn_test_utils.FauxPortLikeObject(category="abcd")
|
||||
)
|
||||
self.assertFalse(exp.eval(obj, net))
|
||||
|
||||
def test_filter_comparator_eval_greater_than(self):
|
||||
exp = tbn.SingleExpression(
|
||||
tbn.Variables.PORT_CATEGORY, tbn.Comparator.GT, "test"
|
||||
)
|
||||
|
||||
obj = tbn.Port.from_ironic_port(
|
||||
tbn_test_utils.FauxPortLikeObject(category="test2")
|
||||
)
|
||||
net = tbn_test_utils.FauxNetwork()
|
||||
self.assertTrue(exp.eval(obj, net))
|
||||
|
||||
obj = tbn.Port.from_ironic_port(
|
||||
tbn_test_utils.FauxPortLikeObject(category="test")
|
||||
)
|
||||
self.assertFalse(exp.eval(obj, net))
|
||||
|
||||
obj = tbn.Port.from_ironic_port(
|
||||
tbn_test_utils.FauxPortLikeObject(category="abcd")
|
||||
)
|
||||
self.assertFalse(exp.eval(obj, net))
|
||||
|
||||
def test_filter_comparator_eval_less_than_or_equal(self):
|
||||
exp = tbn.SingleExpression(
|
||||
tbn.Variables.NETWORK_NAME, tbn.Comparator.LT_OR_EQ, "test"
|
||||
)
|
||||
port = tbn.Port.from_ironic_port(
|
||||
tbn_test_utils.FauxPortLikeObject()
|
||||
)
|
||||
|
||||
net = tbn.Network("fake_id", "test2", [])
|
||||
self.assertFalse(exp.eval(port, net))
|
||||
|
||||
net = tbn.Network("fake_id", "test", [])
|
||||
self.assertTrue(exp.eval(port, net))
|
||||
|
||||
net = tbn.Network("fake_id", "abcd", [])
|
||||
self.assertTrue(exp.eval(port, net))
|
||||
|
||||
def test_filter_comparator_eval_less_than(self):
|
||||
exp = tbn.SingleExpression(
|
||||
tbn.Variables.NETWORK_NAME, tbn.Comparator.LT, "test"
|
||||
)
|
||||
port = tbn.Port.from_ironic_port(
|
||||
tbn_test_utils.FauxPortLikeObject()
|
||||
)
|
||||
|
||||
net = tbn.Network("fake_id", "test2", [])
|
||||
self.assertFalse(exp.eval(port, net))
|
||||
|
||||
net = tbn.Network("fake_id", "test", [])
|
||||
self.assertFalse(exp.eval(port, net))
|
||||
|
||||
net = tbn.Network("fake_id", "abcd", [])
|
||||
self.assertTrue(exp.eval(port, net))
|
||||
|
||||
def test_filter_comparator_eval_prefix_match(self):
|
||||
exp = tbn.SingleExpression(
|
||||
tbn.Variables.PORT_VENDOR, tbn.Comparator.PREFIX_MATCH, "some"
|
||||
)
|
||||
net = tbn_test_utils.FauxNetwork()
|
||||
|
||||
obj = tbn.Port.from_ironic_port(
|
||||
tbn_test_utils.FauxPortLikeObject(vendor="some_vendor")
|
||||
)
|
||||
self.assertTrue(exp.eval(obj, net))
|
||||
|
||||
obj = tbn.Port.from_ironic_port(
|
||||
tbn_test_utils.FauxPortLikeObject(vendor="another_vendor")
|
||||
)
|
||||
self.assertFalse(exp.eval(obj, net))
|
||||
|
||||
def test_filter_comparator_eval_prefix_match_bad_variable_type(self):
|
||||
exp = tbn.SingleExpression(
|
||||
tbn.Variables.PORT_IS_PORT, tbn.Comparator.PREFIX_MATCH, "some"
|
||||
)
|
||||
net = tbn_test_utils.FauxNetwork()
|
||||
|
||||
obj = tbn.Port.from_ironic_port(tbn_test_utils.FauxPortLikeObject())
|
||||
self.assertTrue(obj.is_port())
|
||||
self.assertRaises(exc.TraitBasedNetworkingException,
|
||||
exp.eval, obj, net)
|
||||
|
||||
def test_filter_operator_eval_and(self):
|
||||
exp = tbn.CompoundExpression(
|
||||
tbn.FunctionExpression(tbn.Variables.PORT_IS_PORT),
|
||||
tbn.Operator.AND,
|
||||
tbn.FunctionExpression(tbn.Variables.PORT_IS_PORT),
|
||||
)
|
||||
net = tbn_test_utils.FauxNetwork()
|
||||
|
||||
obj = tbn.Port.from_ironic_port(tbn_test_utils.FauxPortLikeObject())
|
||||
self.assertTrue(exp.eval(obj, net))
|
||||
|
||||
obj = tbn.Portgroup.from_ironic_portgroup(
|
||||
tbn_test_utils.FauxPortLikeObject())
|
||||
self.assertFalse(exp.eval(obj, net))
|
||||
|
||||
def test_filter_operator_eval_or(self):
|
||||
exp = tbn.CompoundExpression(
|
||||
tbn.FunctionExpression(tbn.Variables.PORT_IS_PORT),
|
||||
tbn.Operator.OR,
|
||||
tbn.FunctionExpression(tbn.Variables.PORT_IS_PORT),
|
||||
)
|
||||
net = tbn_test_utils.FauxNetwork()
|
||||
obj = tbn.Port.from_ironic_port(tbn_test_utils.FauxPortLikeObject())
|
||||
self.assertTrue(exp.eval(obj, net))
|
||||
|
||||
obj = tbn.Portgroup.from_ironic_portgroup(
|
||||
tbn_test_utils.FauxPortLikeObject())
|
||||
self.assertFalse(exp.eval(obj, net))
|
||||
|
||||
exp = tbn.CompoundExpression(
|
||||
tbn.FunctionExpression(tbn.Variables.PORT_IS_PORT),
|
||||
tbn.Operator.OR,
|
||||
tbn.FunctionExpression(tbn.Variables.PORT_IS_PORTGROUP),
|
||||
)
|
||||
obj = tbn.Port.from_ironic_port(tbn_test_utils.FauxPortLikeObject())
|
||||
self.assertTrue(exp.eval(obj, net))
|
||||
|
||||
def test_attach_port_equality(self):
|
||||
self.assertEqual(
|
||||
tbn.AttachPort(
|
||||
tbn.TraitAction(
|
||||
"CUSTOM_TRAIT",
|
||||
tbn.Actions.ATTACH_PORT,
|
||||
tbn.FilterExpression.parse(
|
||||
"port.vendor == 'clover'")
|
||||
),
|
||||
"fake_node_uuid",
|
||||
"fake_port_uuid",
|
||||
"fake_network_id"),
|
||||
tbn.AttachPort(
|
||||
tbn.TraitAction(
|
||||
"CUSTOM_TRAIT",
|
||||
tbn.Actions.ATTACH_PORT,
|
||||
tbn.FilterExpression.parse(
|
||||
"port.vendor == 'clover'")
|
||||
),
|
||||
"fake_node_uuid",
|
||||
"fake_port_uuid",
|
||||
"fake_network_id")
|
||||
)
|
||||
self.assertNotEqual(
|
||||
tbn.AttachPort(
|
||||
tbn.TraitAction(
|
||||
"CUSTOM_TRAIT",
|
||||
tbn.Actions.ATTACH_PORT,
|
||||
tbn.FilterExpression.parse(
|
||||
"port.vendor == 'clover'")
|
||||
),
|
||||
"fake_node_uuid",
|
||||
"fake_port_uuid",
|
||||
"fake_network_id"),
|
||||
tbn.AttachPort(
|
||||
tbn.TraitAction(
|
||||
"CUSTOM_TRAIT",
|
||||
tbn.Actions.ATTACH_PORT,
|
||||
tbn.FilterExpression.parse(
|
||||
"port.vendor == 'clover'")
|
||||
),
|
||||
"real_node_uuid",
|
||||
"fake_port_uuid",
|
||||
"fake_network_id")
|
||||
)
|
||||
|
||||
def test_attach_portgroup_equality(self):
|
||||
self.assertEqual(
|
||||
tbn.AttachPortgroup(
|
||||
tbn.TraitAction(
|
||||
"CUSTOM_TRAIT",
|
||||
tbn.Actions.ATTACH_PORTGROUP,
|
||||
tbn.FilterExpression.parse(
|
||||
"port.vendor == 'clover'")
|
||||
),
|
||||
"fake_node_uuid",
|
||||
"fake_portgroup_uuid",
|
||||
"fake_network_id"),
|
||||
|
||||
tbn.AttachPortgroup(
|
||||
tbn.TraitAction(
|
||||
"CUSTOM_TRAIT",
|
||||
tbn.Actions.ATTACH_PORTGROUP,
|
||||
tbn.FilterExpression.parse(
|
||||
"port.vendor == 'clover'")
|
||||
),
|
||||
"fake_node_uuid",
|
||||
"fake_portgroup_uuid",
|
||||
"fake_network_id")
|
||||
)
|
||||
|
||||
self.assertNotEqual(
|
||||
tbn.AttachPortgroup(
|
||||
tbn.TraitAction(
|
||||
"CUSTOM_TRAIT",
|
||||
tbn.Actions.ATTACH_PORTGROUP,
|
||||
tbn.FilterExpression.parse(
|
||||
"port.vendor == 'clover'")
|
||||
),
|
||||
"fake_node_uuid",
|
||||
"fake_portgroup_uuid",
|
||||
"fake_network_id"),
|
||||
tbn.AttachPortgroup(
|
||||
tbn.TraitAction(
|
||||
"CUSTOM_TRAIT",
|
||||
tbn.Actions.ATTACH_PORTGROUP,
|
||||
tbn.FilterExpression.parse(
|
||||
"port.vendor == 'clover'")
|
||||
),
|
||||
"fake_node_uuid",
|
||||
"real_portgroup_uuid",
|
||||
"fake_network_id"),
|
||||
)
|
||||
|
||||
def test_rendered_actions_type_mismatch_equality(self):
|
||||
types = [
|
||||
tbn.RenderedAction(
|
||||
tbn.TraitAction(
|
||||
"CUSTOM_TRAIT",
|
||||
tbn.Actions.ATTACH_PORT,
|
||||
tbn.FilterExpression.parse("port.vendor == 'cogwork'")),
|
||||
"fake_node_uuid"
|
||||
),
|
||||
tbn.AttachPort(
|
||||
tbn.TraitAction(
|
||||
"CUSTOM_TRAIT",
|
||||
tbn.Actions.ATTACH_PORT,
|
||||
tbn.FilterExpression.parse("port.vendor == 'cogwork'")),
|
||||
"fake_node_uuid",
|
||||
"fake_port_uuid",
|
||||
"fake_network_id"
|
||||
),
|
||||
tbn.AttachPortgroup(
|
||||
tbn.TraitAction(
|
||||
"CUSTOM_TRAIT",
|
||||
tbn.Actions.ATTACH_PORTGROUP,
|
||||
tbn.FilterExpression.parse("port.vendor == 'cogwork'")),
|
||||
"fake_node_uuid",
|
||||
"fake_portgroup_uuid",
|
||||
"fake_network_id"
|
||||
),
|
||||
tbn.NoMatch(
|
||||
tbn.TraitAction(
|
||||
"CUSTOM_TRAIT",
|
||||
tbn.Actions.ATTACH_PORTGROUP,
|
||||
tbn.FilterExpression.parse("port.vendor == 'cogwork'")),
|
||||
"fake_node_uuid",
|
||||
"didn't match"
|
||||
)
|
||||
]
|
||||
|
||||
# Make sure we can compare differing types of RenderedActions and not
|
||||
# blow up. Inequal types of RenderedActions must not evaluate as equal
|
||||
# with __eq__/==.
|
||||
for prod in itertools.product(types, repeat=2):
|
||||
if type(prod[0]) is type(prod[1]):
|
||||
self.assertEqual(prod[0], prod[1])
|
||||
else:
|
||||
self.assertNotEqual(prod[0], prod[1])
|
||||
@@ -0,0 +1,150 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import ironic.common.trait_based_networking.base as tbn
|
||||
import ironic.common.trait_based_networking.grammar.parser as tbn_parser
|
||||
|
||||
from ironic.tests import base
|
||||
import ironic.tests.unit.common.trait_based_networking.utils as tbn_test_utils
|
||||
|
||||
import lark
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class TraitBasedNetworkingFilterParserTestCase(base.TestCase):
|
||||
def test_grammar_acceptable_to_lark(self):
|
||||
parser = lark.Lark(
|
||||
tbn_parser.FILTER_EXPRESSION_GRAMMAR,
|
||||
start=tbn_parser.FILTER_EXPRESSION_GRAMMAR_START_RULE)
|
||||
self.assertIsNotNone(parser)
|
||||
|
||||
def test_filter_expression_parser(self):
|
||||
@dataclass
|
||||
class SubTestCase(object):
|
||||
description: str
|
||||
expression: str
|
||||
port_args: list[dict]
|
||||
# TODO(clif): Also test networks here
|
||||
expected_eval_result: list[bool]
|
||||
|
||||
subtests = [
|
||||
SubTestCase(
|
||||
"Basic single expression",
|
||||
"port.vendor == 'vendor_string'",
|
||||
[{"vendor": "vendor_string"}],
|
||||
[True],
|
||||
),
|
||||
SubTestCase(
|
||||
"Compound expressions with parenthesis",
|
||||
(
|
||||
"port.vendor == 'green' "
|
||||
"&& port.category == 'storage' "
|
||||
"&& (port.physical_network =~ 'storage' "
|
||||
"|| port.address == '192.168.1.1')"
|
||||
),
|
||||
[
|
||||
{
|
||||
"vendor": "green",
|
||||
"address": "192.168.1.1",
|
||||
"physical_network": "storagenet",
|
||||
"category": "storage",
|
||||
},
|
||||
{
|
||||
"vendor": "green",
|
||||
"address": "192.168.1.1",
|
||||
"physical_network": "othernet",
|
||||
"category": "storage",
|
||||
},
|
||||
{
|
||||
"vendor": "brown",
|
||||
"address": "192.168.1.1",
|
||||
"physical_network": "storagenet",
|
||||
"category": "storage_alpha",
|
||||
},
|
||||
],
|
||||
[True, True, False],
|
||||
),
|
||||
SubTestCase(
|
||||
"Part 1 ensuring parens are respected",
|
||||
(
|
||||
"(port.vendor == 'green' "
|
||||
"&& port.category == 'test') "
|
||||
"|| port.address == '192.168.1.1'"
|
||||
),
|
||||
[
|
||||
{
|
||||
"vendor": "brown",
|
||||
"address": "192.168.1.1",
|
||||
"category": "prod",
|
||||
},
|
||||
{
|
||||
"vendor": "brown",
|
||||
"address": "192.168.1.1",
|
||||
"category": "prod",
|
||||
},
|
||||
],
|
||||
[True, True],
|
||||
),
|
||||
SubTestCase(
|
||||
"Part 2 ensuring parens are respected",
|
||||
(
|
||||
"port.vendor == 'green' "
|
||||
"&& (port.category == 'test' "
|
||||
"|| port.address == '192.168.1.1')"
|
||||
),
|
||||
[
|
||||
{
|
||||
"vendor": "brown",
|
||||
"address": "192.168.1.1",
|
||||
"category": "prod",
|
||||
},
|
||||
{
|
||||
"vendor": "brown",
|
||||
"address": "192.168.1.1",
|
||||
"category": "prod",
|
||||
},
|
||||
],
|
||||
[False, False],
|
||||
),
|
||||
# TODO(clif): Test case about network variables
|
||||
]
|
||||
|
||||
for subtest in subtests:
|
||||
with self.subTest(subtest=subtest):
|
||||
# Assert expression parses correctly.
|
||||
result = tbn.FilterExpression.parse(subtest.expression)
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
# Assert the transformation of the parse tree to
|
||||
# trait based networking objects renders back to the original
|
||||
# expression exactly.
|
||||
self.assertEqual(str(result), subtest.expression)
|
||||
|
||||
self.assertEqual(
|
||||
len(subtest.port_args), len(subtest.expected_eval_result)
|
||||
)
|
||||
|
||||
# Assert the evaluation of the expression returns the correct
|
||||
# result.
|
||||
for i in range(0, len(subtest.port_args)):
|
||||
self.assertEqual(
|
||||
result.eval(
|
||||
tbn.Port.from_ironic_port(
|
||||
tbn_test_utils.FauxPortLikeObject(
|
||||
**subtest.port_args[i]
|
||||
)
|
||||
),
|
||||
tbn.Network("test_net_id", "test_network", []),
|
||||
),
|
||||
subtest.expected_eval_result[i],
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
class FauxPortLikeObject(object):
|
||||
def __init__(
|
||||
self,
|
||||
port_id="fake_id",
|
||||
uuid="fake_uuid",
|
||||
address="test",
|
||||
category="cat",
|
||||
physical_network="test_physnet",
|
||||
vendor="fake_vendor",
|
||||
):
|
||||
self.id = port_id
|
||||
self.uuid = uuid
|
||||
self.address = address
|
||||
self.category = category
|
||||
self.physical_network = physical_network
|
||||
self.vendor = vendor
|
||||
|
||||
|
||||
class FauxNetwork(object):
|
||||
def __init__(
|
||||
self,
|
||||
network_id="fake_net_id",
|
||||
name="test_network",
|
||||
tags=['test_tag'],
|
||||
):
|
||||
self.id = network_id
|
||||
self.name = name
|
||||
self.tags = tags
|
||||
@@ -48,3 +48,4 @@ websockify>=0.9.0 # LGPLv3
|
||||
PyYAML>=6.0.2 # MIT
|
||||
cheroot>=10.0.1 # BSD
|
||||
cotyledon>=2.0.0 # Apache-2.0
|
||||
lark>=1.2.2 # MIT
|
||||
|
||||
Reference in New Issue
Block a user