summaryrefslogtreecommitdiff
path: root/neutron/tests/functional/agent/ovn/metadata/test_metadata_agent.py
blob: d0d4aebac3ad4f1d77593e894db1b9e94343e732 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# Copyright 2020 Red Hat, Inc.
# All Rights Reserved.
#
#    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 re
from unittest import mock

from oslo_config import fixture as fixture_config
from oslo_utils import uuidutils
from ovsdbapp.backend.ovs_idl import event
from ovsdbapp.backend.ovs_idl import idlutils
from ovsdbapp.tests.functional.schema.ovn_southbound import event as test_event

from neutron.agent.linux import iptables_manager
from neutron.agent.ovn.metadata import agent
from neutron.agent.ovn.metadata import ovsdb
from neutron.agent.ovn.metadata import server as metadata_server
from neutron.common.ovn import constants as ovn_const
from neutron.common import utils as n_utils
from neutron.conf.agent.metadata import config as meta_config
from neutron.conf.agent.ovn.metadata import config as meta_config_ovn
from neutron.tests.common import net_helpers
from neutron.tests.functional import base


class MetadataAgentHealthEvent(event.WaitEvent):
    event_name = 'MetadataAgentHealthEvent'

    def __init__(self, chassis, sb_cfg, table, timeout=5):
        self.chassis = chassis
        self.sb_cfg = sb_cfg
        super(MetadataAgentHealthEvent, self).__init__(
            (self.ROW_UPDATE,), table, (('name', '=', self.chassis),),
            timeout=timeout)

    def matches(self, event, row, old=None):
        if not super(MetadataAgentHealthEvent, self).matches(event, row, old):
            return False
        return int(row.external_ids.get(
            ovn_const.OVN_AGENT_METADATA_SB_CFG_KEY, 0)) >= self.sb_cfg


class MetadataPortCreateEvent(event.WaitEvent):
    event_name = 'MetadataPortCreateEvent'

    def __init__(self, metadata_port, timeout=5):
        table = 'Port_Binding'
        events = (self.ROW_CREATE,)
        conditions = (('logical_port', '=', metadata_port),)
        super(MetadataPortCreateEvent, self).__init__(
            events, table, conditions, timeout=timeout
        )


class TestMetadataAgent(base.TestOVNFunctionalBase):
    OVN_BRIDGE = 'br-int'
    FAKE_CHASSIS_HOST = 'ovn-host-fake'

    def setUp(self):
        super(TestMetadataAgent, self).setUp()
        self.handler = self.sb_api.idl.notify_handler
        # We only have OVN NB and OVN SB running for functional tests
        self.mock_ovsdb_idl = mock.Mock()
        mock_metadata_instance = mock.Mock()
        mock_metadata_instance.start.return_value = self.mock_ovsdb_idl
        mock_metadata = mock.patch.object(
            ovsdb, 'MetadataAgentOvsIdl').start()
        mock_metadata.return_value = mock_metadata_instance
        self._mock_get_ovn_br = mock.patch.object(
            agent.MetadataAgent,
            '_get_ovn_bridge',
            return_value=self.OVN_BRIDGE).start()
        self.agent = self._start_metadata_agent()

    @property
    def agent_chassis_table(self):
        if self.agent.has_chassis_private:
            return 'Chassis_Private'
        return 'Chassis'

    def _start_metadata_agent(self):
        conf = self.useFixture(fixture_config.Config()).conf
        conf.register_opts(meta_config.SHARED_OPTS)
        conf.register_opts(meta_config.UNIX_DOMAIN_METADATA_PROXY_OPTS)
        conf.register_opts(meta_config.METADATA_PROXY_HANDLER_OPTS)
        conf.register_opts(meta_config_ovn.OVS_OPTS, group='ovs')
        meta_config_ovn.setup_privsep()

        ovn_sb_db = self.ovsdb_server_mgr.get_ovsdb_connection_path('sb')
        conf.set_override('ovn_sb_connection', ovn_sb_db, group='ovn')
        conf.set_override('metadata_workers', '0')

        self.chassis_name = self.add_fake_chassis(self.FAKE_CHASSIS_HOST)
        mock.patch.object(agent.MetadataAgent,
                          '_get_own_chassis_name',
                          return_value=self.chassis_name).start()
        agt = agent.MetadataAgent(conf)
        with mock.patch.object(metadata_server.UnixDomainMetadataProxy,
                               'wait'):
            agt.start()

        # Metadata agent will open connections to OVS and SB databases.
        # Close connections to them when the test ends,
        self.addCleanup(agt.ovs_idl.ovsdb_connection.stop)
        self.addCleanup(agt.sb_idl.ovsdb_connection.stop)

        return agt

    def test_metadata_agent_healthcheck(self):
        chassis_row = self.sb_api.db_find(
            self.agent_chassis_table,
            ('name', '=', self.chassis_name)).execute(
            check_error=True)[0]

        # Assert that, prior to creating a resource the metadata agent
        # didn't populate the external_ids from the Chassis
        self.assertNotIn(ovn_const.OVN_AGENT_METADATA_SB_CFG_KEY,
                         chassis_row['external_ids'])

        # Let's list the agents to force the nb_cfg to be bumped on NB
        # db, which will automatically increment the nb_cfg counter on
        # NB_Global and make ovn-controller copy it over to SB_Global. Upon
        # this event, Metadata agent will update the external_ids on its
        # Chassis row to signal that it's healthy.

        row_event = MetadataAgentHealthEvent(self.chassis_name, 1,
                                             self.agent_chassis_table)
        self.handler.watch_event(row_event)
        self.new_list_request('agents').get_response(self.api)

        # If we do not time out waiting for the event, then we are assured
        # that the metadata agent has populated the external_ids from the
        # chassis with the nb_cfg, 1 revisions when listing the agents.
        self.assertTrue(row_event.wait())

    def _create_metadata_port(self, txn, lswitch_name, port_name=None):
        mdt_port_name = port_name or 'ovn-mdt-' + uuidutils.generate_uuid()
        txn.add(
            self.nb_api.lsp_add(
                lswitch_name,
                mdt_port_name,
                type=ovn_const.LSP_TYPE_LOCALPORT,
                addresses='AA:AA:AA:AA:AA:AA 192.168.122.123',
                external_ids={
                    ovn_const.OVN_CIDRS_EXT_ID_KEY: '192.168.122.123/24',
                    ovn_const.OVN_DEVID_EXT_ID_KEY: 'ovnmeta-' + lswitch_name
                }))

    def _update_metadata_port_ip(self, metadata_port_name):
        external_ids = {
            ovn_const.OVN_CIDRS_EXT_ID_KEY: "192.168.122.2/24",
            ovn_const.OVN_DEVID_EXT_ID_KEY:
                'ovnmeta-' + uuidutils.generate_uuid()
        }
        self.nb_api.set_lswitch_port(lport_name=metadata_port_name,
                                     external_ids=external_ids).execute()

    def _create_logical_switch_port(self, type_=None, addresses=None):
        lswitch_name = 'ovn-' + uuidutils.generate_uuid()
        lswitchport_name = 'ovn-port-' + uuidutils.generate_uuid()
        # It may take some time to ovn-northd to translate from OVN NB DB to
        # the OVN SB DB. Wait for port binding event to happen before binding
        # the port to chassis.
        pb_event = test_event.WaitForPortBindingEvent(lswitchport_name)
        self.handler.watch_event(pb_event)

        lswitch_port_columns = {}
        if addresses:
            lswitch_port_columns['addresses'] = addresses
        if type_:
            lswitch_port_columns['type'] = type_

        with self.nb_api.transaction(check_error=True, log_errors=True) as txn:
            txn.add(
                self.nb_api.ls_add(lswitch_name))
            txn.add(
                self.nb_api.create_lswitch_port(
                    lswitchport_name, lswitch_name, **lswitch_port_columns))
            self._create_metadata_port(txn, lswitch_name)
        self.assertTrue(pb_event.wait())

        return lswitchport_name, lswitch_name

    def test_agent_resync_on_non_existing_bridge(self):
        BR_NEW = 'br-new'
        self._mock_get_ovn_br.return_value = BR_NEW
        self.agent.ovs_idl.list_br.return_value.execute.return_value = [BR_NEW]
        # The agent has initialized with br-int and above list_br doesn't
        # return it, hence the agent should trigger reconfiguration and store
        # new br-new value to its attribute.
        self.assertEqual(self.OVN_BRIDGE, self.agent.ovn_bridge)

        # NOTE: The IP address is specifically picked such that it fits the
        # metadata port external_ids: { neutron:cidrs }. This is because agent
        # will only trigger if the logical port is part of a neutron subnet
        lswitchport_name, _ = self._create_logical_switch_port(
            addresses='AA:AA:AA:AA:AA:AB 192.168.122.125'
        )

        # Trigger PortBindingChassisCreatedEvent
        self.sb_api.lsp_bind(lswitchport_name, self.chassis_name).execute(
            check_error=True, log_errors=True)

        exc = Exception("Agent bridge hasn't changed from %s to %s "
                        "in 10 seconds after Port_Binding event" %
                        (self.agent.ovn_bridge, BR_NEW))
        n_utils.wait_until_true(
            lambda: BR_NEW == self.agent.ovn_bridge,
            timeout=10,
            exception=exc)

    def _test_agent_events(self, delete, type_=None, update=False):
        m_pb_created = mock.patch.object(
            agent.PortBindingChassisCreatedEvent, 'run').start()
        m_pb_deleted = mock.patch.object(
            agent.PortBindingChassisDeletedEvent, 'run').start()
        m_pb_updated = mock.patch.object(
            agent.PortBindingMetaPortUpdatedEvent, 'run').start()

        lswitchport_name, lswitch_name = self._create_logical_switch_port(
            type_)
        self.sb_api.lsp_bind(lswitchport_name, self.chassis_name).execute(
            check_error=True, log_errors=True)
        if update and type_ == ovn_const.LSP_TYPE_LOCALPORT:
            with self.nb_api.transaction(
                    check_error=True, log_errors=True) as txn:
                mdt_port_name = 'ovn-mdt-' + uuidutils.generate_uuid()
                metadata_port_create_event = MetadataPortCreateEvent(
                    mdt_port_name)
                self.agent.sb_idl.idl.notify_handler.watch_event(
                    metadata_port_create_event)
                self._create_metadata_port(txn, lswitch_name, mdt_port_name)
            self.assertTrue(metadata_port_create_event.wait())

            self.sb_api.lsp_bind(mdt_port_name, self.chassis_name).execute(
                check_error=True, log_errors=True)
            self._update_metadata_port_ip(mdt_port_name)

        def pb_created():
            if m_pb_created.call_count < 1:
                return False
            args = m_pb_created.call_args[0]
            self.assertEqual('update', args[0])
            self.assertEqual(self.chassis_name, args[1].chassis[0].name)
            self.assertFalse(args[2].chassis)
            return True

        n_utils.wait_until_true(
            pb_created,
            timeout=10,
            exception=Exception(
                "PortBindingChassisCreatedEvent didn't happen on port "
                "binding."))

        def pb_updated():
            if m_pb_updated.call_count < 1:
                return False
            args = m_pb_updated.call_args[0]
            self.assertEqual('update', args[0])
            self.assertTrue(args[1].external_ids)
            self.assertTrue(args[2].external_ids)
            device_id = args[1].external_ids.get(
                ovn_const.OVN_DEVID_EXT_ID_KEY, "")
            self.assertTrue(device_id.startswith("ovnmeta-"))
            new_cidrs = args[1].external_ids.get(
                ovn_const.OVN_CIDRS_EXT_ID_KEY, "")
            old_cidrs = args[2].external_ids.get(
                ovn_const.OVN_CIDRS_EXT_ID_KEY, "")
            self.assertNotEqual(new_cidrs, old_cidrs)
            self.assertNotEqual(old_cidrs, "")
            return True
        if update and type_ == ovn_const.LSP_TYPE_LOCALPORT:
            n_utils.wait_until_true(
                pb_updated,
                timeout=10,
                exception=Exception(
                    "PortBindingMetaPortUpdatedEvent didn't happen on "
                    "metadata port ip address updated."))

        if delete:
            self.nb_api.delete_lswitch_port(
                lswitchport_name, lswitch_name).execute(
                    check_error=True, log_errors=True)
        else:
            self.sb_api.lsp_unbind(lswitchport_name).execute(
                check_error=True, log_errors=True)

        def pb_deleted():
            if m_pb_deleted.call_count < 1:
                return False
            args = m_pb_deleted.call_args[0]
            if delete:
                self.assertEqual('delete', args[0])
                self.assertTrue(args[1].chassis)
                self.assertEqual(self.chassis_name, args[1].chassis[0].name)
            else:
                self.assertEqual('update', args[0])
                self.assertFalse(args[1].chassis)
                self.assertEqual(self.chassis_name, args[2].chassis[0].name)
            return True

        n_utils.wait_until_true(
            pb_deleted,
            timeout=10,
            exception=Exception(
                "PortBindingChassisDeletedEvent didn't happen on port "
                "unbind or delete."))

        self.assertEqual(1, m_pb_deleted.call_count)

    def test_agent_unbind_port(self):
        self._test_agent_events(delete=False)

    def test_agent_delete_bound_external_port(self):
        self._test_agent_events(delete=True, type_='external')

    def test_agent_delete_bound_nonexternal_port(self):
        with mock.patch.object(agent.LOG, 'warning') as m_warn:
            self._test_agent_events(delete=True)
        self.assertTrue(m_warn.called)

    def test_agent_registration_at_chassis_create_event(self):
        def check_for_metadata():
            chassis = self.sb_api.lookup(
                self.agent_chassis_table, self.chassis_name)
            return ovn_const.OVN_AGENT_METADATA_ID_KEY in chassis.external_ids

        exc = Exception('Chassis not created, %s is not in chassis '
                        'external_ids' % ovn_const.OVN_AGENT_METADATA_ID_KEY)
        n_utils.wait_until_true(check_for_metadata, timeout=5, exception=exc)

        # Delete Chassis and assert
        chassis = self.sb_api.lookup('Chassis', self.chassis_name)
        self.del_fake_chassis(chassis.name)
        self.assertRaises(idlutils.RowNotFound, self.sb_api.lookup,
                          'Chassis', self.chassis_name)

        # Re-add the Chassis
        self.add_fake_chassis(self.FAKE_CHASSIS_HOST, name=self.chassis_name)
        exc = Exception('Agent metadata failed to re-register itself '
                        'after the Chassis %s was re-created' %
                        self.chassis_name)

        # Check if metadata agent was re-registered
        chassis = self.sb_api.lookup('Chassis', self.chassis_name)
        n_utils.wait_until_true(
            check_for_metadata,
            timeout=10,
            exception=exc)

    def test_agent_metadata_port_ip_update_event(self):
        self._test_agent_events(
            delete=False, type_=ovn_const.LSP_TYPE_LOCALPORT, update=True)

    def test_metadata_agent_only_monitors_own_chassis(self):
        # We already have the fake chassis which we should be monitoring, so
        # create an event looking for a change to another chassis
        other_name = uuidutils.generate_uuid()
        other_chassis = self.add_fake_chassis(self.FAKE_CHASSIS_HOST,
                                              name=other_name)
        self.assertEqual(other_chassis, other_name)

        event = MetadataAgentHealthEvent(chassis=other_name, sb_cfg=-1,
                                         table=self.agent_chassis_table,
                                         timeout=0)
        # Use the agent's sb_idl to watch for the event since it has condition
        self.agent.sb_idl.idl.notify_handler.watch_event(event)
        # Use the test sb_api to set other_chassis values since shouldn't exist
        # on agent's sb_idl
        self.sb_api.db_set(
            self.agent_chassis_table, other_chassis,
            ('external_ids', {'test': 'value'})).execute(check_error=True)

        event2 = MetadataAgentHealthEvent(chassis=self.chassis_name, sb_cfg=-1,
                                          table=self.agent_chassis_table)
        self.agent.sb_idl.idl.notify_handler.watch_event(event2)
        # Use the test's sb_api again to send a command so we can see if it
        # completes and short-circuit the need to wait for a timeout to pass
        # the test. If we get the result to this, we would have gotten the
        # previous result as well.
        self.sb_api.db_set(
            self.agent_chassis_table, self.chassis_name,
            ('external_ids', {'test': 'value'})).execute(check_error=True)
        self.assertTrue(event2.wait())
        self.assertFalse(event.wait())

    def test__ensure_datapath_checksum_if_dpdk(self):
        self.mock_ovsdb_idl.db_get.return_value.execute.return_value = (
            ovn_const.CHASSIS_DATAPATH_NETDEV)
        regex = re.compile(r'-A POSTROUTING -p tcp -m tcp '
                           r'-j CHECKSUM --checksum-fill')
        namespace = self.useFixture(net_helpers.NamespaceFixture()).name
        self.agent._ensure_datapath_checksum(namespace)
        iptables_mgr = iptables_manager.IptablesManager(
            use_ipv6=True, nat=False, namespace=namespace, external_lock=False)
        for rule in iptables_mgr.get_rules_for_table('mangle'):
            if regex.match(rule):
                return
        else:
            self.fail('Rule not found in "mangle" table, in namespace %s' %
                      namespace)

    def test_metadata_proxy_handler_idl(self):
        # This test relies on the configuration option metadata_workers=0
        proxy_sb_idl = self.agent._proxy.server._server._application.sb_idl
        agent_sb_idl = self.agent.sb_idl
        self.assertEqual(agent_sb_idl, proxy_sb_idl)