diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py index 23a841d7a10..9cbdaa831ac 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py @@ -505,6 +505,11 @@ class OVNClient(object): context, txn, port, addr_pairs_diff.removed, unset=True) + # Keep key value pairs that were in the original external ids + # of the ovn port and we did not touch. + for k, v in ovn_port.external_ids.items(): + external_ids.setdefault(k, v) + # NOTE(lizk): Fail port updating if port doesn't exist. This # prevents any new inserted resources to be orphan, such as port # dhcp options or ACL rules for port, e.g. a port was created diff --git a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovn_db_resources.py b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovn_db_resources.py index 101a8c52654..e8a9fdafa8c 100644 --- a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovn_db_resources.py +++ b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovn_db_resources.py @@ -13,6 +13,8 @@ # under the License. +import copy + import mock import netaddr @@ -1049,6 +1051,69 @@ class TestDNSRecords(base.TestOVNFunctionalBase): self._validate_dns_records([]) +class TestPortExternalIds(base.TestOVNFunctionalBase): + + def _get_lsp_external_id(self, port_id): + ovn_port = self.nb_api.lookup('Logical_Switch_Port', port_id) + return copy.deepcopy(ovn_port.external_ids) + + def _set_lsp_external_id(self, port_id, **pairs): + external_ids = self._get_lsp_external_id(port_id) + for key, val in pairs.items(): + external_ids[key] = val + self.nb_api.set_lswitch_port(lport_name=port_id, + external_ids=external_ids).execute() + + def _create_lsp(self): + n1 = self._make_network(self.fmt, 'n1', True) + res = self._create_subnet(self.fmt, n1['network']['id'], '10.0.0.0/24') + subnet = self.deserialize(self.fmt, res)['subnet'] + p = self._make_port(self.fmt, n1['network']['id'], + fixed_ips=[{'subnet_id': subnet['id']}]) + port_id = p['port']['id'] + return port_id, self._get_lsp_external_id(port_id) + + def test_port_update_has_ext_ids(self): + port_id, ext_ids = self._create_lsp() + self.assertIsNotNone(ext_ids) + + def test_port_update_add_ext_id(self): + port_id, ext_ids = self._create_lsp() + ext_ids['another'] = 'value' + self._set_lsp_external_id(port_id, another='value') + self.assertEqual(ext_ids, self._get_lsp_external_id(port_id)) + + def test_port_update_change_ext_id_value(self): + port_id, ext_ids = self._create_lsp() + ext_ids['another'] = 'value' + self._set_lsp_external_id(port_id, another='value') + self.assertEqual(ext_ids, self._get_lsp_external_id(port_id)) + ext_ids['another'] = 'value2' + self._set_lsp_external_id(port_id, another='value2') + self.assertEqual(ext_ids, self._get_lsp_external_id(port_id)) + + def test_port_update_with_foreign_ext_ids(self): + port_id, ext_ids = self._create_lsp() + new_ext_ids = {ovn_const.OVN_PORT_FIP_EXT_ID_KEY: '1.11.11.1', + 'foreign_key2': 'value1234'} + self._set_lsp_external_id(port_id, **new_ext_ids) + ext_ids.update(new_ext_ids) + self.assertEqual(ext_ids, self._get_lsp_external_id(port_id)) + # invoke port update and make sure the the values we added to the + # external_ids remain undisturbed. + data = {'port': {'extra_dhcp_opts': [{'ip_version': 4, + 'opt_name': 'ip-forward-enable', + 'opt_value': '0'}]}} + port_req = self.new_update_request('ports', data, port_id) + port_req.get_response(self.api) + actual_ext_ids = self._get_lsp_external_id(port_id) + # update port should have not removed keys it does not use from the + # external ids of the lsp. + self.assertEqual('1.11.11.1', + actual_ext_ids.get(ovn_const.OVN_PORT_FIP_EXT_ID_KEY)) + self.assertEqual('value1234', actual_ext_ids.get('foreign_key2')) + + class TestNBDbResourcesOverTcp(TestNBDbResources): def get_ovsdb_server_protocol(self): return 'tcp'