diff --git a/neutron/cmd/ovn/neutron_ovn_db_sync_util.py b/neutron/cmd/ovn/neutron_ovn_db_sync_util.py index d3bb2658e0b..79bd8f38cc6 100644 --- a/neutron/cmd/ovn/neutron_ovn_db_sync_util.py +++ b/neutron/cmd/ovn/neutron_ovn_db_sync_util.py @@ -24,6 +24,7 @@ from neutron.conf.agent import securitygroups_rpc from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf from neutron import manager from neutron import opts as neutron_options +from neutron.plugins.ml2.drivers.ovn import db_migration from neutron.plugins.ml2.drivers.ovn.mech_driver import mech_driver from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import impl_idl_ovn from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovn_db_sync @@ -171,6 +172,11 @@ def main(): logging.setup(conf, 'neutron_ovn_db_sync_util') LOG.info('Started Neutron OVN db sync') mode = ovn_conf.get_ovn_neutron_sync_mode() + # Migrate mode will run as repair mode in the synchronizer + migrate = False + if mode == ovn_conf.MIGRATE_MODE: + mode = ovn_db_sync.SYNC_MODE_REPAIR + migrate = True if mode not in [ovn_db_sync.SYNC_MODE_LOG, ovn_db_sync.SYNC_MODE_REPAIR]: LOG.error( 'Invalid sync mode : ["%s"]. Should be "log" or "repair"', mode) @@ -235,3 +241,8 @@ def main(): LOG.info('Sync for Southbound db started with mode : %s', mode) sb_synchronizer.do_sync() LOG.info('Sync completed for Southbound db') + + if migrate: + LOG.info("Migrating Neutron database from OVS to OVN") + db_migration.migrate_neutron_database_to_ovn(core_plugin) + LOG.info("Neutron database migration from OVS to OVN completed") diff --git a/neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py b/neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py index ded41c5920c..e8652081904 100644 --- a/neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py +++ b/neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py @@ -26,6 +26,8 @@ EXTRA_LOG_LEVEL_DEFAULTS = [ VLOG_LEVELS = {'CRITICAL': vlog.CRITICAL, 'ERROR': vlog.ERROR, 'WARNING': vlog.WARN, 'INFO': vlog.INFO, 'DEBUG': vlog.DEBUG} +MIGRATE_MODE = "migrate" + ovn_opts = [ cfg.StrOpt('ovn_nb_connection', default='tcp:127.0.0.1:6641', @@ -85,7 +87,7 @@ ovn_opts = [ 'to 60 seconds.')), cfg.StrOpt('neutron_sync_mode', default='log', - choices=('off', 'log', 'repair'), + choices=('off', 'log', 'repair', MIGRATE_MODE), help=_('The synchronization mode of OVN_Northbound OVSDB ' 'with Neutron DB.\n' 'off - synchronization is off \n' @@ -97,7 +99,11 @@ ovn_opts = [ 'repair - during neutron-server startup, automatically' ' create resources found in Neutron but not in OVN.' ' Also remove resources from OVN' - ' that are no longer in Neutron.')), + ' that are no longer in Neutron.' + '%(migrate)s - This mode is to OVS to OVN migration. It' + ' will sync the DB just like repair mode but it will' + ' additionally fix the Neutron DB resource from OVS to' + ' OVN.') % {'migrate': MIGRATE_MODE}), cfg.BoolOpt('ovn_l3_mode', default=True, deprecated_for_removal=True, diff --git a/neutron/plugins/ml2/drivers/ovn/db_migration.py b/neutron/plugins/ml2/drivers/ovn/db_migration.py new file mode 100644 index 00000000000..c5e3d8c78f9 --- /dev/null +++ b/neutron/plugins/ml2/drivers/ovn/db_migration.py @@ -0,0 +1,83 @@ +# Copyright 2021 Red Hat, Inc. +# +# 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. + +from neutron_lib.api.definitions import portbindings as pb_api +from neutron_lib import context as n_context +from neutron_lib.db import api as db_api + +from neutron.db.models.plugins.ml2 import geneveallocation +from neutron.db.models.plugins.ml2 import vxlanallocation +from neutron.objects import network as network_obj +from neutron.objects import ports as port_obj +from neutron.objects import trunk as trunk_obj + +VIF_DETAILS_TO_REMOVE = ( + pb_api.OVS_HYBRID_PLUG, + pb_api.VIF_DETAILS_BRIDGE_NAME, + pb_api.VIF_DETAILS_CONNECTIVITY) + + +def migrate_neutron_database_to_ovn(plugin): + """Change DB content from OVS to OVN mech driver. + + - Changes vxlan network type to Geneve and updates Geneve allocations. + - Removes unnecessary settings from port binding vif details, such as + connectivity, bridge_name and ovs_hybrid_plug, as they are not used by + OVN. + """ + ctx = n_context.get_admin_context() + with db_api.CONTEXT_WRITER.using(ctx) as session: + # Change network type from vxlan geneve + segments = network_obj.NetworkSegment.get_objects( + ctx, network_type='vxlan') + for segment in segments: + segment.network_type = 'geneve' + segment.update() + # Update Geneve allocation for the segment + session.query(geneveallocation.GeneveAllocation).filter( + geneveallocation.GeneveAllocation.geneve_vni == + segment.segmentation_id).update({"allocated": True}) + # Zero Vxlan allocations + session.query(vxlanallocation.VxlanAllocation).filter( + vxlanallocation.VxlanAllocation.vxlan_vni == + segment.segmentation_id).update({"allocated": False}) + + port_bindings = port_obj.PortBinding.get_objects( + ctx, vif_type='ovs', vnic_type='normal', status='ACTIVE') + for pb in port_bindings: + if not pb.vif_details: + continue + vif_details = pb.vif_details.copy() + for detail in VIF_DETAILS_TO_REMOVE: + try: + del vif_details[detail] + except KeyError: + pass + if vif_details != pb.vif_details: + pb.vif_details = vif_details + pb.update() + + for trunk in trunk_obj.Trunk.get_objects(ctx): + for subport in trunk.sub_ports: + pbs = port_obj.PortBinding.get_objects( + ctx, port_id=subport.port_id) + for pb in pbs: + profile = {} + if pb.profile: + profile = pb.profile.copy() + profile['parent_name'] = trunk.port_id + profile['tag'] = subport.segmentation_id + if profile != pb.profile: + pb.profile = profile + pb.update() diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py index b8c526a76db..3bff20c276a 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py @@ -66,7 +66,7 @@ from neutron.tests.unit.plugins.ml2 import test_security_group OVN_PROFILE = ovn_const.OVN_PORT_BINDING_PROFILE -class TestOVNMechanismDriver(test_plugin.Ml2PluginV2TestCase): +class TestOVNMechanismDriverBase(test_plugin.Ml2PluginV2TestCase): _mechanism_drivers = ['logger', 'ovn'] _extension_drivers = ['port_security', 'dns'] @@ -87,7 +87,7 @@ class TestOVNMechanismDriver(test_plugin.Ml2PluginV2TestCase): group='ovn') cfg.CONF.set_override('vlan_transparent', True) mock.patch.object(impl_idl_ovn.Backend, 'schema_helper').start() - super(TestOVNMechanismDriver, self).setUp() + super().setUp() mm = directory.get_plugin().mechanism_manager self.mech_driver = mm.mech_drivers['ovn'].obj neutron_agent.AgentCache(self.mech_driver) @@ -121,6 +121,8 @@ class TestOVNMechanismDriver(test_plugin.Ml2PluginV2TestCase): p.start() self.addCleanup(p.stop) + +class TestOVNMechanismDriver(TestOVNMechanismDriverBase): @mock.patch.object(ovsdb_monitor.OvnInitPGNbIdl, 'from_server') @mock.patch.object(ovsdb_monitor, 'short_living_ovsdb_api') def test__create_neutron_pg_drop_non_existing( diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/test_db_migration.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/test_db_migration.py new file mode 100644 index 00000000000..a70e84f67cb --- /dev/null +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/test_db_migration.py @@ -0,0 +1,158 @@ +# Copyright 2021 Red Hat, Inc. +# +# 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. + +from neutron_lib.api.definitions import portbindings as pb +from neutron_lib.api.definitions import provider_net as pnet +from neutron_lib import context as n_context +from neutron_lib.db import api as db_api +from oslo_utils import uuidutils + +from neutron.db.models.plugins.ml2 import geneveallocation +from neutron.db.models.plugins.ml2 import vxlanallocation +from neutron.objects import ports as port_obj +from neutron.objects import trunk as trunk_obj +from neutron.plugins.ml2.drivers.ovn import db_migration +from neutron.tests.unit.plugins.ml2.drivers.ovn.mech_driver import ( + test_mech_driver) + + +class TestMigrateNeutronDatabaseToOvn( + test_mech_driver.TestOVNMechanismDriverBase): + + def _create_ml2_ovs_test_resources(self, vif_details_list): + self.subport_profiles = {} + ctx = n_context.get_admin_context() + for sid in range(1, 6): + net_arg = {pnet.NETWORK_TYPE: 'vxlan', + pnet.SEGMENTATION_ID: sid} + network_id = self._make_network(self.fmt, 'net%d' % sid, True, + arg_list=(pnet.NETWORK_TYPE, + pnet.SEGMENTATION_ID,), + **net_arg)['network']['id'] + + for vif_details in vif_details_list: + port = self._make_port(self.fmt, network_id)['port'] + port_o = port_obj.PortBinding.get_object( + ctx, port_id=port['id'], host='') + port_o.vif_type = 'ovs' + port_o.vif_details = vif_details + port_o.update() + + for i in range(1, 4): + port = self._make_port(self.fmt, network_id)['port'] + subport1 = self._make_port(self.fmt, network_id)['port'] + subport2 = self._make_port(self.fmt, network_id)['port'] + + trunk_id = uuidutils.generate_uuid() + + subports = [trunk_obj.SubPort( + ctx, + port_id=subport1['id'], + trunk_id=trunk_id, + segmentation_type="vlan", + segmentation_id=i * 10 + j) for j in range(2)] + + trunk = trunk_obj.Trunk( + ctx, + id=trunk_id, + port_id=port['id'], + project_id='foo', + subports=subports) + trunk.create() + + subport_pb = port_obj.PortBinding.get_object( + ctx, port_id=subport1['id'], host='') + self.assertFalse(subport_pb.profile) + + self.subport_profiles[subport1['id']] = {"parent_name": port['id'], + "tag": i * 10} + self.subport_profiles[subport2['id']] = {"parent_name": port['id'], + "tag": i * 10 + 1} + + # set something to the last subport port binding + subport_pb = port_obj.PortBinding.get_object( + ctx, port_id=subport2['id'], host='') + # need to generate new id + subport_pb.profile = subport_pb.profile.copy() + subport_pb.profile['foo'] = 'bar' + subport_pb.update() + + self.subport_profiles[subport2['id']]["foo"] = "bar" + + def _validate_resources_after_migration(self, expected_vif_details): + ctx = n_context.get_admin_context() + + # Check network types + networks = self.plugin.get_networks(ctx) + for network in networks: + self.assertEqual("geneve", network["provider:network_type"]) + + with db_api.CONTEXT_READER.using(ctx) as session: + # Check there are no vxlan allocations + vxlan_allocations = session.query( + vxlanallocation.VxlanAllocation).filter( + vxlanallocation.VxlanAllocation.allocated == True # noqa + ).all() + self.assertFalse(vxlan_allocations) + + # Check all the networks have Geneve allocations + geneve_allocations = session.query( + geneveallocation.GeneveAllocation).filter( + geneveallocation.GeneveAllocation.allocated == True # noqa + ).all() + self.assertEqual(len(networks), len(geneve_allocations)) + + # Check port bindings vif details are as expected + ports = self.plugin.get_ports(ctx) + for port in ports: + self.assertIn(port['binding:vif_details'], expected_vif_details) + + # Check port profiles for subport ports + for trunk in trunk_obj.Trunk.get_objects(ctx): + for subport in trunk.sub_ports: + port = self.plugin.get_port(ctx, id=subport.port_id) + self.assertEqual( + self.subport_profiles[subport.port_id], + port["binding:profile"]) + + def test_db_migration(self): + """Test the DB migration + + It creates 5 vxlan networks, each should get a vxlan vni allocated. + Then it creates 3 ports with different vif details. + + After the DB migration the vxlan networks should not be allocated but + be geneve type and have geneve allocations. Also the port binding vif + details should not contain hybrid plugging, bridge name for trunk and + l2 connectivity for OVS agent. + """ + vif_details_list = [ + {pb.CAP_PORT_FILTER: "true", + pb.OVS_HYBRID_PLUG: "true", + pb.VIF_DETAILS_BRIDGE_NAME: "foo", + pb.VIF_DETAILS_CONNECTIVITY: "l2"}, + {pb.CAP_PORT_FILTER: "true", + pb.VIF_DETAILS_BRIDGE_NAME: "foo"}, + {"foo": "bar"}, + {}, + ] + expected_vif_details = [ + {pb.CAP_PORT_FILTER: "true"}, + {"foo": "bar"}, + {}, + ] + + self._create_ml2_ovs_test_resources(vif_details_list) + db_migration.migrate_neutron_database_to_ovn(self.mech_driver._plugin) + self._validate_resources_after_migration(expected_vif_details) diff --git a/tools/ovn_migration/tripleo_environment/playbooks/ovn-migration.yml b/tools/ovn_migration/tripleo_environment/playbooks/ovn-migration.yml index 28c5a86ae5b..724c182a73d 100644 --- a/tools/ovn_migration/tripleo_environment/playbooks/ovn-migration.yml +++ b/tools/ovn_migration/tripleo_environment/playbooks/ovn-migration.yml @@ -34,15 +34,6 @@ - migration -# It runs tasks on ovn-dbs nodes -# 1. Change vxlan network type to Geneve -- name: Prepare controllers - hosts: ovn-dbs - roles: - - prepare-controllers - tags: - - migration - # # TripleO / Director is executed to deploy ovn using "br-migration" for the # dataplane, while br-int is left intact to avoid dataplane disruption. diff --git a/tools/ovn_migration/tripleo_environment/playbooks/roles/migration/tasks/sync-dbs.yml b/tools/ovn_migration/tripleo_environment/playbooks/roles/migration/tasks/sync-dbs.yml index df7c7d5aa9e..24a6910959b 100644 --- a/tools/ovn_migration/tripleo_environment/playbooks/roles/migration/tasks/sync-dbs.yml +++ b/tools/ovn_migration/tripleo_environment/playbooks/roles/migration/tasks/sync-dbs.yml @@ -8,13 +8,13 @@ command: podman exec "{{ neutron_id.stdout }}" neutron-ovn-db-sync-util --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugins/ml2/ml2_conf.ini - --ovn-neutron_sync_mode repair + --ovn-neutron_sync_mode migrate - name: Sync neutron db with OVN db (container) - Run 2 command: podman exec "{{ neutron_id.stdout }}" neutron-ovn-db-sync-util --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugins/ml2/ml2_conf.ini - --ovn-neutron_sync_mode repair + --ovn-neutron_sync_mode migrate - name: Pause and let ovn-controllers settle before doing the final activation (5 minute) pause: minutes=5 diff --git a/tools/ovn_migration/tripleo_environment/playbooks/roles/prepare-controllers/defaults/main.yml b/tools/ovn_migration/tripleo_environment/playbooks/roles/prepare-controllers/defaults/main.yml deleted file mode 100644 index 47b301794db..00000000000 --- a/tools/ovn_migration/tripleo_environment/playbooks/roles/prepare-controllers/defaults/main.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -neutron_conf_path: /var/lib/config-data/puppet-generated/neutron/etc/neutron/neutron.conf -neutron_conf_tempfile: /tmp/neutron.conf diff --git a/tools/ovn_migration/tripleo_environment/playbooks/roles/prepare-controllers/tasks/main.yml b/tools/ovn_migration/tripleo_environment/playbooks/roles/prepare-controllers/tasks/main.yml deleted file mode 100644 index 13419328fea..00000000000 --- a/tools/ovn_migration/tripleo_environment/playbooks/roles/prepare-controllers/tasks/main.yml +++ /dev/null @@ -1,27 +0,0 @@ ---- -- name: Fetch neutron configuration - fetch: - src: "{{ neutron_conf_path }}" - dest: "{{ neutron_conf_tempfile }}" - flat: yes - when: ovn_central is defined - -- name: Get DB connection string - set_fact: - db_connection_string: "{{ lookup('ini', 'connection section=database file={{ neutron_conf_tempfile }}') }}" - when: ovn_central is defined - -# The shell below is not readable well. The code spawns a sqlalchemy engine -# and connects to the Neutron database to run following SQL command: -# UPDATE networksegments SET networksegments.network_type='geneve' WHERE networksegments.network_type='vxlan'; -# The indented Python code looks as follows: -# -# from sqlalchemy import create_engine -# -# engine = create_engine("{{ mysql_url.stdout }}") -# with engine.connect() as conn: -# conn.execute("SQL COMMAND") -# -- name: Change vxlan networks to Geneve - shell: podman exec -it neutron_api python3 -c $'from sqlalchemy import create_engine\nengine = create_engine("{{ db_connection_string }}")\nwith engine.connect() as conn:\n\tconn.execute("update networksegments set networksegments.network_type=\'geneve\' where networksegments.network_type=\'vxlan\';")' - when: ovn_central is defined