Merge "ovn-migration: Introduce migrate mode to DB sync tool"

This commit is contained in:
Zuul 2021-04-26 14:22:47 +00:00 committed by Gerrit Code Review
commit 603951809a
9 changed files with 266 additions and 45 deletions

View File

@ -24,6 +24,7 @@ from neutron.conf.agent import securitygroups_rpc
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf
from neutron import manager from neutron import manager
from neutron import opts as neutron_options 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 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 impl_idl_ovn
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovn_db_sync 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') logging.setup(conf, 'neutron_ovn_db_sync_util')
LOG.info('Started Neutron OVN db sync') LOG.info('Started Neutron OVN db sync')
mode = ovn_conf.get_ovn_neutron_sync_mode() 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]: if mode not in [ovn_db_sync.SYNC_MODE_LOG, ovn_db_sync.SYNC_MODE_REPAIR]:
LOG.error( LOG.error(
'Invalid sync mode : ["%s"]. Should be "log" or "repair"', mode) '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) LOG.info('Sync for Southbound db started with mode : %s', mode)
sb_synchronizer.do_sync() sb_synchronizer.do_sync()
LOG.info('Sync completed for Southbound db') 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")

View File

@ -26,6 +26,8 @@ EXTRA_LOG_LEVEL_DEFAULTS = [
VLOG_LEVELS = {'CRITICAL': vlog.CRITICAL, 'ERROR': vlog.ERROR, 'WARNING': VLOG_LEVELS = {'CRITICAL': vlog.CRITICAL, 'ERROR': vlog.ERROR, 'WARNING':
vlog.WARN, 'INFO': vlog.INFO, 'DEBUG': vlog.DEBUG} vlog.WARN, 'INFO': vlog.INFO, 'DEBUG': vlog.DEBUG}
MIGRATE_MODE = "migrate"
ovn_opts = [ ovn_opts = [
cfg.StrOpt('ovn_nb_connection', cfg.StrOpt('ovn_nb_connection',
default='tcp:127.0.0.1:6641', default='tcp:127.0.0.1:6641',
@ -85,7 +87,7 @@ ovn_opts = [
'to 60 seconds.')), 'to 60 seconds.')),
cfg.StrOpt('neutron_sync_mode', cfg.StrOpt('neutron_sync_mode',
default='log', default='log',
choices=('off', 'log', 'repair'), choices=('off', 'log', 'repair', MIGRATE_MODE),
help=_('The synchronization mode of OVN_Northbound OVSDB ' help=_('The synchronization mode of OVN_Northbound OVSDB '
'with Neutron DB.\n' 'with Neutron DB.\n'
'off - synchronization is off \n' 'off - synchronization is off \n'
@ -97,7 +99,11 @@ ovn_opts = [
'repair - during neutron-server startup, automatically' 'repair - during neutron-server startup, automatically'
' create resources found in Neutron but not in OVN.' ' create resources found in Neutron but not in OVN.'
' Also remove resources from 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', cfg.BoolOpt('ovn_l3_mode',
default=True, default=True,
deprecated_for_removal=True, deprecated_for_removal=True,

View File

@ -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()

View File

@ -66,7 +66,7 @@ from neutron.tests.unit.plugins.ml2 import test_security_group
OVN_PROFILE = ovn_const.OVN_PORT_BINDING_PROFILE OVN_PROFILE = ovn_const.OVN_PORT_BINDING_PROFILE
class TestOVNMechanismDriver(test_plugin.Ml2PluginV2TestCase): class TestOVNMechanismDriverBase(test_plugin.Ml2PluginV2TestCase):
_mechanism_drivers = ['logger', 'ovn'] _mechanism_drivers = ['logger', 'ovn']
_extension_drivers = ['port_security', 'dns'] _extension_drivers = ['port_security', 'dns']
@ -87,7 +87,7 @@ class TestOVNMechanismDriver(test_plugin.Ml2PluginV2TestCase):
group='ovn') group='ovn')
cfg.CONF.set_override('vlan_transparent', True) cfg.CONF.set_override('vlan_transparent', True)
mock.patch.object(impl_idl_ovn.Backend, 'schema_helper').start() mock.patch.object(impl_idl_ovn.Backend, 'schema_helper').start()
super(TestOVNMechanismDriver, self).setUp() super().setUp()
mm = directory.get_plugin().mechanism_manager mm = directory.get_plugin().mechanism_manager
self.mech_driver = mm.mech_drivers['ovn'].obj self.mech_driver = mm.mech_drivers['ovn'].obj
neutron_agent.AgentCache(self.mech_driver) neutron_agent.AgentCache(self.mech_driver)
@ -121,6 +121,8 @@ class TestOVNMechanismDriver(test_plugin.Ml2PluginV2TestCase):
p.start() p.start()
self.addCleanup(p.stop) self.addCleanup(p.stop)
class TestOVNMechanismDriver(TestOVNMechanismDriverBase):
@mock.patch.object(ovsdb_monitor.OvnInitPGNbIdl, 'from_server') @mock.patch.object(ovsdb_monitor.OvnInitPGNbIdl, 'from_server')
@mock.patch.object(ovsdb_monitor, 'short_living_ovsdb_api') @mock.patch.object(ovsdb_monitor, 'short_living_ovsdb_api')
def test__create_neutron_pg_drop_non_existing( def test__create_neutron_pg_drop_non_existing(

View File

@ -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)

View File

@ -34,15 +34,6 @@
- migration - 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 # TripleO / Director is executed to deploy ovn using "br-migration" for the
# dataplane, while br-int is left intact to avoid dataplane disruption. # dataplane, while br-int is left intact to avoid dataplane disruption.

View File

@ -8,13 +8,13 @@
command: podman exec "{{ neutron_id.stdout }}" command: podman exec "{{ neutron_id.stdout }}"
neutron-ovn-db-sync-util --config-file /etc/neutron/neutron.conf neutron-ovn-db-sync-util --config-file /etc/neutron/neutron.conf
--config-file /etc/neutron/plugins/ml2/ml2_conf.ini --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 - name: Sync neutron db with OVN db (container) - Run 2
command: podman exec "{{ neutron_id.stdout }}" command: podman exec "{{ neutron_id.stdout }}"
neutron-ovn-db-sync-util --config-file /etc/neutron/neutron.conf neutron-ovn-db-sync-util --config-file /etc/neutron/neutron.conf
--config-file /etc/neutron/plugins/ml2/ml2_conf.ini --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) - name: Pause and let ovn-controllers settle before doing the final activation (5 minute)
pause: minutes=5 pause: minutes=5

View File

@ -1,3 +0,0 @@
---
neutron_conf_path: /var/lib/config-data/puppet-generated/neutron/etc/neutron/neutron.conf
neutron_conf_tempfile: /tmp/neutron.conf

View File

@ -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