Skip to content

Commit 7b376c7

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "[OVN] OVN agent retrieval filter matching improvement" into stable/2025.1
2 parents dcd0d3e + b958f4e commit 7b376c7

File tree

4 files changed

+151
-13
lines changed

4 files changed

+151
-13
lines changed

neutron/common/ovn/constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@
9696
OVN_CONTROLLER_TYPES = (OVN_CONTROLLER_AGENT,
9797
OVN_CONTROLLER_GW_AGENT,
9898
)
99+
OVN_AGENT_TYPES = (OVN_CONTROLLER_AGENT,
100+
OVN_CONTROLLER_GW_AGENT,
101+
OVN_METADATA_AGENT,
102+
OVN_NEUTRON_AGENT,
103+
)
99104

100105
# OVN ACLs have priorities. The highest priority ACL that matches is the one
101106
# that takes effect. Our choice of priority numbers is arbitrary, but it

neutron/common/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,3 +1115,10 @@ def stringmap(data: abc.Mapping[str, typing.Any],
11151115
for key, value in data.items():
11161116
result[key] = default if value is None else str(value)
11171117
return result
1118+
1119+
1120+
def is_iterable_not_string(value):
1121+
"""Return if a value is iterable but not a string type"""
1122+
return (isinstance(value, abc.Iterable) and
1123+
not isinstance(value, abc.ByteString) and
1124+
not isinstance(value, str))

neutron/plugins/ml2/drivers/ovn/agent/neutron_agent.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import datetime
1818

1919
from oslo_config import cfg
20+
from oslo_log import log as logging
2021
from oslo_utils import timeutils
2122

2223
from neutron._i18n import _
@@ -25,6 +26,9 @@
2526
from neutron.common import utils
2627

2728

29+
LOG = logging.getLogger(__name__)
30+
31+
2832
class DeletedChassis:
2933
external_ids = {}
3034
hostname = '("Chassis" register deleted)'
@@ -291,8 +295,25 @@ def agents_by_chassis_private(self, chassis_private):
291295
def get_agents(self, filters=None):
292296
filters = filters or {}
293297
agent_list = []
298+
type_errors = {}
294299
for agent in self:
295300
agent_dict = agent.as_dict()
296-
if all(agent_dict[k] in v for k, v in filters.items()):
301+
for k, v in filters.items():
302+
if isinstance(agent_dict[k], type(v)):
303+
if agent_dict[k] != v:
304+
break
305+
else:
306+
if utils.is_iterable_not_string(v):
307+
if agent_dict[k] not in v:
308+
break
309+
else:
310+
type_errors[k] = (type(agent_dict[k]), v)
311+
break
312+
else:
297313
agent_list.append(agent)
314+
for field, (field_type, value) in type_errors.items():
315+
LOG.info(f'Value "{value}" {type(value)} does not '
316+
f'match the OVN related agent field "{field}" '
317+
f'with type {field_type}')
318+
298319
return agent_list

neutron/tests/unit/plugins/ml2/drivers/ovn/agent/test_neutron_agent.py

Lines changed: 117 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414

15+
import collections
1516
import datetime
17+
import random
1618
from unittest import mock
1719

1820
import eventlet
@@ -30,18 +32,43 @@ def setUp(self):
3032
super().setUp()
3133
self.agent_cache = neutron_agent.AgentCache(driver=mock.ANY)
3234
self.addCleanup(self._clean_agent_cache)
33-
self.names_ref = []
34-
for i in range(10): # Add 10 agents.
35+
self.agents = {}
36+
self.num_agents = 10 # Add 10 agents.
37+
for i in range(self.num_agents):
38+
agent_type = random.choice(ovn_const.OVN_AGENT_TYPES)
39+
other_config = {}
40+
if agent_type == ovn_const.OVN_CONTROLLER_GW_AGENT:
41+
# 'enable-chassis-as-gw' is mandatory if the controller is
42+
# a gateway chassis; if not, it will default to
43+
# 'OVN Controller agent'. Check ``ControllerGatewayAgent``
44+
# class.
45+
other_config = {'ovn-cms-options': 'enable-chassis-as-gw'}
3546
chassis = fakes.FakeOvsdbRow.create_one_ovsdb_row(
36-
attrs={'other_config': {}})
47+
attrs={'other_config': other_config,
48+
'hostname': f'host{i:d}',
49+
})
50+
ext_ids = {}
51+
if agent_type == ovn_const.OVN_METADATA_AGENT:
52+
ext_ids = {
53+
ovn_const.OVN_AGENT_METADATA_ID_KEY: 'chassis' + str(i)}
54+
elif agent_type == ovn_const.OVN_NEUTRON_AGENT:
55+
ext_ids = {
56+
ovn_const.OVN_AGENT_NEUTRON_ID_KEY: 'chassis' + str(i)}
3757
chassis_private = fakes.FakeOvsdbRow.create_one_ovsdb_row(
3858
attrs={'name': 'chassis' + str(i),
3959
'other_config': {},
4060
'chassis': [chassis],
41-
'nb_cfg_timestamp': timeutils.utcnow_ts() * 1000})
42-
self.agent_cache.update(ovn_const.OVN_CONTROLLER_AGENT,
43-
chassis_private)
44-
self.names_ref.append('chassis' + str(i))
61+
'nb_cfg_timestamp': timeutils.utcnow_ts() * 1000,
62+
'external_ids': ext_ids,
63+
})
64+
self.agent_cache.update(agent_type, chassis_private)
65+
self.agents['chassis' + str(i)] = agent_type
66+
67+
self.assertEqual(self.num_agents, len(list(self.agent_cache)))
68+
for agent_class in (neutron_agent.NeutronAgent,
69+
neutron_agent.MetadataAgent,
70+
neutron_agent.OVNNeutronAgent):
71+
mock.patch.object(agent_class, 'alive', return_value=True).start()
4572

4673
def _clean_agent_cache(self):
4774
del self.agent_cache
@@ -69,18 +96,19 @@ def test_update_while_iterating_agents(self):
6996
pool.spawn(self._list_agents)
7097
pool.spawn(self._add_and_delete_agents)
7198
pool.waitall()
72-
self.assertEqual(self.names_ref, self.names_read)
99+
self.assertEqual(list(self.agents.keys()), self.names_read)
73100

74101
def test_agents_by_chassis_private(self):
102+
ext_ids = {ovn_const.OVN_AGENT_METADATA_ID_KEY: 'chassis5'}
75103
chassis_private = fakes.FakeOvsdbRow.create_one_ovsdb_row(
76-
attrs={'name': 'chassis5'})
104+
attrs={'name': 'chassis5',
105+
'external_ids': ext_ids})
77106
agents = self.agent_cache.agents_by_chassis_private(chassis_private)
78107
agents = list(agents)
79108
self.assertEqual(1, len(agents))
80109
self.assertEqual('chassis5', agents[0].agent_id)
81110

82-
@mock.patch.object(neutron_agent.ControllerAgent, 'alive')
83-
def test_heartbeat_timestamp_format(self, agent_alive):
111+
def test_heartbeat_timestamp_format(self):
84112
chassis_private = fakes.FakeOvsdbRow.create_one_ovsdb_row(
85113
attrs={'name': 'chassis5'})
86114
agents = self.agent_cache.agents_by_chassis_private(chassis_private)
@@ -89,8 +117,85 @@ def test_heartbeat_timestamp_format(self, agent_alive):
89117
agent.updated_at = datetime.datetime(
90118
year=2023, month=2, day=23, hour=1, minute=2, second=3,
91119
microsecond=456789).replace(tzinfo=datetime.timezone.utc)
92-
agent_alive.return_value = True
93120

94121
# Verify that both microseconds and timezone are dropped
95122
self.assertEqual(str(agent.as_dict()['heartbeat_timestamp']),
96123
'2023-02-23 01:02:03')
124+
125+
def test_list_agents_filtering_host_same_type(self):
126+
for idx in range(len(self.agents)):
127+
host = f'host{idx:d}'
128+
agents = self.agent_cache.get_agents(filters={'host': host})
129+
self.assertEqual(1, len(agents))
130+
self.assertEqual(host, agents[0].as_dict()['host'])
131+
132+
def test_list_agents_filtering_host_as_iterable(self):
133+
hosts = []
134+
for idx in range(len(self.agents)):
135+
hosts.append(f'host{idx:d}')
136+
137+
agents = self.agent_cache.get_agents(filters={'host': hosts})
138+
self.assertEqual(len(self.agents), len(agents))
139+
140+
def test_list_agents_filtering_agent_type_same_type(self):
141+
agent_types = collections.defaultdict(int)
142+
for _type in self.agents.values():
143+
agent_types[_type] = agent_types[_type] + 1
144+
145+
for _type in agent_types:
146+
agents = self.agent_cache.get_agents(
147+
filters={'agent_type': _type})
148+
self.assertEqual(agent_types[_type], len(agents))
149+
self.assertEqual(_type, agents[0].as_dict()['agent_type'])
150+
151+
def test_list_agents_filtering_agent_type_as_iterable(self):
152+
agents = self.agent_cache.get_agents(
153+
filters={'agent_type': ovn_const.OVN_AGENT_TYPES})
154+
self.assertEqual(self.num_agents, len(agents))
155+
156+
@mock.patch.object(neutron_agent, 'LOG')
157+
def test_list_agents_filtering_wrong_type(self, mock_log):
158+
agents = self.agent_cache.get_agents(filters={'host': 111})
159+
self.assertEqual(0, len(agents))
160+
mock_log.info.assert_called_once()
161+
162+
def test_list_agents_filtering_same_string_in_filter(self):
163+
# As reported in LP#2110094, if two registers have the same substring,
164+
# the filter didn't work.
165+
# Chassis 1, hostname: compute-0
166+
chassis = fakes.FakeOvsdbRow.create_one_ovsdb_row(
167+
attrs={'other_config': {},
168+
'hostname': 'compute-0'})
169+
chassis_private = fakes.FakeOvsdbRow.create_one_ovsdb_row(
170+
attrs={'name': 'chassis1',
171+
'other_config': {},
172+
'chassis': [chassis],
173+
'nb_cfg_timestamp': timeutils.utcnow_ts() * 1000,
174+
'external_ids': {}})
175+
self.agent_cache.update(ovn_const.OVN_CONTROLLER_AGENT,
176+
chassis_private)
177+
178+
# Chassis 2, hostname: dcn1-compute-0
179+
chassis = fakes.FakeOvsdbRow.create_one_ovsdb_row(
180+
attrs={'other_config': {},
181+
'hostname': 'dcn1-compute-0'})
182+
chassis_private = fakes.FakeOvsdbRow.create_one_ovsdb_row(
183+
attrs={'name': 'chassis2',
184+
'other_config': {},
185+
'chassis': [chassis],
186+
'nb_cfg_timestamp': timeutils.utcnow_ts() * 1000,
187+
'external_ids': {}})
188+
self.agent_cache.update(ovn_const.OVN_CONTROLLER_AGENT,
189+
chassis_private)
190+
191+
agents = self.agent_cache.get_agents(
192+
filters={'host': 'compute-0'})
193+
self.assertEqual(1, len(agents))
194+
195+
agents = self.agent_cache.get_agents(
196+
filters={'host': 'dcn1-compute-0'})
197+
self.assertEqual(1, len(agents))
198+
199+
agents = self.agent_cache.get_agents(
200+
filters={'host': ['compute-0', 'dcn1-compute-0']})
201+
self.assertEqual(2, len(agents))

0 commit comments

Comments
 (0)