ovn-migration: Introduce migrate mode to DB sync tool
The new "migrate" mode for the DB sync utility changes DB contents as follows: - it changes vxlan networks to Geneve, including its allocation in order to avoid future collisions when creating new geneve networks - it removes settings from ports' vif_details that are no longer needed, such as hybrid plugging or bridge_name for the trunk bridges - it sets profile for subports - OVN doesn't use trunk_details but port profile to store data about trunk. Subports have tag and parent_name fileds. Previously, the vxlan to Geneve change was done via ansible role. The tasks in the role were replaced by the script therefore the role is removed. Signed-off-by: Jakub Libosvar <libosvar@redhat.com> Change-Id: I29a39108d9fddb30050ec63a1cdf6bba0400e136
This commit is contained in:
parent
07c0cc4a66
commit
ae78e812d1
@ -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")
|
||||
|
@ -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,
|
||||
|
83
neutron/plugins/ml2/drivers/ovn/db_migration.py
Normal file
83
neutron/plugins/ml2/drivers/ovn/db_migration.py
Normal 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()
|
@ -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(
|
||||
|
158
neutron/tests/unit/plugins/ml2/drivers/ovn/test_db_migration.py
Normal file
158
neutron/tests/unit/plugins/ml2/drivers/ovn/test_db_migration.py
Normal 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)
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -1,3 +0,0 @@
|
||||
---
|
||||
neutron_conf_path: /var/lib/config-data/puppet-generated/neutron/etc/neutron/neutron.conf
|
||||
neutron_conf_tempfile: /tmp/neutron.conf
|
@ -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
|
Loading…
Reference in New Issue
Block a user