Skip to content

Commit 8c26736

Browse files
ralonsohumago
authored andcommitted
Improve "sync_ha_chassis_group" method
The method "sync_ha_chassis_group" now creates (or retrieves) a HA Chassis Group register and updates the needed HA Chassis registers in a single transaction. That is possible using the new ovsdbapp release 2.2.1 (check the depends-on patch). Depends-On: https://review.opendev.org/c/openstack/ovsdbapp/+/871836 Conflicts: neutron/common/ovn/utils.py neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl.py neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py Related-Bug: #1995078 Change-Id: I936855214c635de0e89d5d13a86562f5b282633c (cherry picked from commit ac231c8)
1 parent 98a3eae commit 8c26736

File tree

10 files changed

+265
-145
lines changed

10 files changed

+265
-145
lines changed

neutron/common/ovn/utils.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import copy
1515
import inspect
1616
import os
17+
import random
1718

1819
import netaddr
1920
from neutron_lib.api.definitions import external_net
@@ -34,6 +35,7 @@
3435
from oslo_serialization import jsonutils
3536
from oslo_utils import netutils
3637
from oslo_utils import strutils
38+
from ovsdbapp.backend.ovs_idl import rowview
3739
from ovsdbapp import constants as ovsdbapp_const
3840
import tenacity
3941

@@ -860,6 +862,79 @@ def get_ovn_chassis_other_config(chassis):
860862
return chassis.external_ids
861863

862864

865+
def sync_ha_chassis_group(context, network_id, nb_idl, sb_idl, txn):
866+
"""Return the UUID of the HA Chassis Group or the HA Chassis Group cmd.
867+
868+
Given the Neutron Network ID, this method will return (or create
869+
and then return) the appropriate HA Chassis Group the external
870+
port (in that network) needs to be associated with.
871+
872+
:param context: Neutron API context.
873+
:param network_id: The Neutron network ID.
874+
:param nb_idl: OVN NB IDL
875+
:param sb_idl: OVN SB IDL
876+
:param txn: The ovsdbapp transaction object.
877+
:returns: The HA Chassis Group UUID or the HA Chassis Group command object.
878+
"""
879+
plugin = directory.get_plugin()
880+
az_hints = common_utils.get_az_hints(
881+
plugin.get_network(context, network_id))
882+
883+
ha_ch_grp_name = ovn_name(network_id)
884+
ext_ids = {constants.OVN_AZ_HINTS_EXT_ID_KEY: ','.join(az_hints)}
885+
hcg_cmd = txn.add(nb_idl.ha_chassis_group_add(
886+
ha_ch_grp_name, may_exist=True, external_ids=ext_ids))
887+
888+
if isinstance(hcg_cmd.result, rowview.RowView):
889+
# The HA chassis group existed before this transaction.
890+
ha_ch_grp = hcg_cmd.result
891+
else:
892+
# The HA chassis group is being created in this transaction.
893+
ha_ch_grp = None
894+
895+
# Get the chassis belonging to the AZ hints
896+
ch_list = sb_idl.get_gateway_chassis_from_cms_options(name_only=False)
897+
if not az_hints:
898+
az_chassis = get_gateway_chassis_without_azs(ch_list)
899+
else:
900+
az_chassis = get_chassis_in_azs(ch_list, az_hints)
901+
902+
priority = constants.HA_CHASSIS_GROUP_HIGHEST_PRIORITY
903+
if ha_ch_grp:
904+
# Remove any chassis that no longer belongs to the AZ hints
905+
all_ch = {ch.chassis_name for ch in ha_ch_grp.ha_chassis}
906+
ch_to_del = all_ch - az_chassis
907+
for ch in ch_to_del:
908+
txn.add(nb_idl.ha_chassis_group_del_chassis(
909+
ha_ch_grp_name, ch, if_exists=True))
910+
911+
# Find the highest priority chassis in the HA Chassis Group. If
912+
# it exists and still belongs to the same AZ, keep it as the
913+
# highest priority in the group to avoid ports already bond to it
914+
# from moving to another chassis.
915+
high_prio_ch = max(ha_ch_grp.ha_chassis, key=lambda x: x.priority,
916+
default=None)
917+
priority = constants.HA_CHASSIS_GROUP_HIGHEST_PRIORITY
918+
if high_prio_ch and high_prio_ch.chassis_name in az_chassis:
919+
txn.add(nb_idl.ha_chassis_group_add_chassis(
920+
ha_ch_grp_name, high_prio_ch.chassis_name,
921+
priority=priority))
922+
az_chassis.remove(high_prio_ch.chassis_name)
923+
priority -= 1
924+
925+
# Randomize the order so that networks belonging to the same
926+
# availability zones do not necessarily end up with the same
927+
# Chassis as the highest priority one.
928+
for ch in random.sample(list(az_chassis), len(az_chassis)):
929+
txn.add(nb_idl.ha_chassis_group_add_chassis(
930+
hcg_cmd, ch, priority=priority))
931+
priority -= 1
932+
933+
# Return the existing register UUID or the HA chassis group creation
934+
# command (see ovsdbapp ``HAChassisGroupAddChassisCommand`` class).
935+
return ha_ch_grp.uuid if ha_ch_grp else hcg_cmd
936+
937+
863938
def get_subnets_address_scopes(context, subnets, fixed_ips, ml2_plugin):
864939
"""Returns the IPv4 and IPv6 address scopes of several subnets.
865940

neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/commands.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from oslo_utils import timeutils
1818
from ovsdbapp.backend.ovs_idl import command
1919
from ovsdbapp.backend.ovs_idl import idlutils
20+
from ovsdbapp import utils as ovsdbapp_utils
2021

2122
from neutron._i18n import _
2223
from neutron.common.ovn import constants as ovn_const
@@ -140,6 +141,14 @@ def run_idl(self, txn):
140141
port.dhcpv6_options = dhcpv6_options
141142
else:
142143
port.dhcpv6_options = [dhcpv6_options.result]
144+
145+
# NOTE(ralonsoh): HA chassis group is created by Neutron, there is no
146+
# need to create it in this command.
147+
ha_chassis_group = self.columns.pop('ha_chassis_group', None)
148+
if ha_chassis_group:
149+
hcg_uuid = ovsdbapp_utils.get_uuid(ha_chassis_group)
150+
port.ha_chassis_group = hcg_uuid
151+
143152
for col, val in self.columns.items():
144153
setattr(port, col, val)
145154
# add the newly created port to existing lswitch
@@ -205,6 +214,21 @@ def run_idl(self, txn):
205214
external_ids[k] = v
206215
port.external_ids = external_ids
207216

217+
# NOTE(ralonsoh): HA chassis group is created by Neutron, there is no
218+
# need to create it in this command. The register is also deleted when
219+
# the network to which the HA chassis group is associated is deleted.
220+
ha_chassis_group = self.columns.pop('ha_chassis_group', None)
221+
if ha_chassis_group:
222+
hcg_uuid = ovsdbapp_utils.get_uuid(ha_chassis_group)
223+
try:
224+
port_hcg_uuid = port.ha_chassis_group[0].uuid
225+
except IndexError:
226+
port_hcg_uuid = None
227+
if port_hcg_uuid != hcg_uuid:
228+
port.ha_chassis_group = hcg_uuid
229+
elif ha_chassis_group == []:
230+
port.ha_chassis_group = []
231+
208232
for col, val in self.columns.items():
209233
setattr(port, col, val)
210234

neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -605,15 +605,10 @@ def check_for_ha_chassis_group(self):
605605
network_id = port.external_ids[
606606
ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY].replace(
607607
ovn_const.OVN_NAME_PREFIX, '')
608-
ha_ch_grp = self._ovn_client.sync_ha_chassis_group(
609-
context, network_id, txn)
610-
try:
611-
port_ha_ch_uuid = port.ha_chassis_group[0].uuid
612-
except IndexError:
613-
port_ha_ch_uuid = None
614-
if port_ha_ch_uuid != ha_ch_grp:
615-
txn.add(self._nb_idl.set_lswitch_port(
616-
port.name, ha_chassis_group=ha_ch_grp))
608+
ha_ch_grp = utils.sync_ha_chassis_group(
609+
context, network_id, self._nb_idl, self._sb_idl, txn)
610+
txn.add(self._nb_idl.set_lswitch_port(
611+
port.name, ha_chassis_group=ha_ch_grp))
617612

618613
self._delete_default_ha_chassis_group(txn)
619614

neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py

Lines changed: 9 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import collections
1717
import copy
1818
import datetime
19-
import random
2019

2120
import netaddr
2221
from neutron_lib.api.definitions import l3
@@ -469,76 +468,6 @@ def _get_port_options(self, port):
469468
bp_info.vnic_type, bp_info.capabilities
470469
)
471470

472-
def sync_ha_chassis_group(self, context, network_id, txn):
473-
"""Return the UUID of the HA Chassis Group.
474-
475-
Given the Neutron Network ID, this method will return (or create
476-
and then return) the appropriate HA Chassis Group the external
477-
port (in that network) needs to be associated with.
478-
479-
:param context: Neutron API context.
480-
:param network_id: The Neutron network ID.
481-
:param txn: The ovsdbapp transaction object.
482-
:returns: An HA Chassis Group UUID.
483-
"""
484-
az_hints = common_utils.get_az_hints(
485-
self._plugin.get_network(context, network_id))
486-
487-
ha_ch_grp_name = utils.ovn_name(network_id)
488-
# FIXME(lucasagomes): Couldn't find a better way of doing this
489-
# without a sub-transaction. This shouldn't be a problem since
490-
# the HA Chassis Group associated with a network will be deleted
491-
# as part of the network delete method (if present)
492-
with self._nb_idl.create_transaction(check_error=True) as sub_txn:
493-
sub_txn.add(self._nb_idl.ha_chassis_group_add(
494-
ha_ch_grp_name, may_exist=True))
495-
496-
ha_ch_grp = self._nb_idl.ha_chassis_group_get(
497-
ha_ch_grp_name).execute(check_error=True)
498-
txn.add(self._nb_idl.db_set(
499-
'HA_Chassis_Group', ha_ch_grp_name,
500-
('external_ids',
501-
{ovn_const.OVN_AZ_HINTS_EXT_ID_KEY: ','.join(az_hints)})))
502-
503-
# Get the chassis belonging to the AZ hints
504-
ch_list = self._sb_idl.get_gateway_chassis_from_cms_options(
505-
name_only=False)
506-
if not az_hints:
507-
az_chassis = utils.get_gateway_chassis_without_azs(ch_list)
508-
else:
509-
az_chassis = utils.get_chassis_in_azs(ch_list, az_hints)
510-
511-
# Remove any chassis that no longer belongs to the AZ hints
512-
all_ch = {ch.chassis_name for ch in ha_ch_grp.ha_chassis}
513-
ch_to_del = all_ch - az_chassis
514-
for ch in ch_to_del:
515-
txn.add(self._nb_idl.ha_chassis_group_del_chassis(
516-
ha_ch_grp_name, ch, if_exists=True))
517-
518-
# Find the highest priority chassis in the HA Chassis Group. If
519-
# it exists and still belongs to the same AZ, keep it as the highest
520-
# priority in the group to avoid ports already bond to it from
521-
# moving to another chassis.
522-
high_prio_ch = max(ha_ch_grp.ha_chassis, key=lambda x: x.priority,
523-
default=None)
524-
priority = ovn_const.HA_CHASSIS_GROUP_HIGHEST_PRIORITY
525-
if high_prio_ch and high_prio_ch.chassis_name in az_chassis:
526-
txn.add(self._nb_idl.ha_chassis_group_add_chassis(
527-
ha_ch_grp_name, high_prio_ch.chassis_name,
528-
priority=priority))
529-
az_chassis.remove(high_prio_ch.chassis_name)
530-
priority -= 1
531-
532-
# Randomize the order so that networks belonging to the same
533-
# availability zones do not necessarily end up with the same
534-
# Chassis as the highest priority one.
535-
for ch in random.sample(list(az_chassis), len(az_chassis)):
536-
txn.add(self._nb_idl.ha_chassis_group_add_chassis(
537-
ha_ch_grp_name, ch, priority=priority))
538-
priority -= 1
539-
540-
return ha_ch_grp.uuid
541-
542471
def update_port_dhcp_options(self, port_info, txn):
543472
dhcpv4_options = []
544473
dhcpv6_options = []
@@ -620,9 +549,9 @@ def create_port(self, context, port):
620549

621550
if (self.is_external_ports_supported() and
622551
port_info.type == ovn_const.LSP_TYPE_EXTERNAL):
623-
kwargs['ha_chassis_group'] = (
624-
self.sync_ha_chassis_group(
625-
context, port['network_id'], txn))
552+
kwargs['ha_chassis_group'] = utils.sync_ha_chassis_group(
553+
context, port['network_id'], self._nb_idl, self._sb_idl,
554+
txn)
626555

627556
# NOTE(mjozefcz): Do not set addresses if the port is not
628557
# bound, has no device_owner and it is OVN LB VIP port.
@@ -743,8 +672,9 @@ def update_port(self, context, port, port_object=None):
743672
if self.is_external_ports_supported():
744673
if port_info.type == ovn_const.LSP_TYPE_EXTERNAL:
745674
columns_dict['ha_chassis_group'] = (
746-
self.sync_ha_chassis_group(
747-
context, port['network_id'], txn))
675+
utils.sync_ha_chassis_group(
676+
context, port['network_id'], self._nb_idl,
677+
self._sb_idl, txn))
748678
else:
749679
# Clear the ha_chassis_group field
750680
columns_dict['ha_chassis_group'] = []
@@ -2105,7 +2035,9 @@ def update_network(self, context, network, original_network=None):
21052035
neutron_net_azs = lswitch_params['external_ids'].get(
21062036
ovn_const.OVN_AZ_HINTS_EXT_ID_KEY, '')
21072037
if ovn_ls_azs != neutron_net_azs:
2108-
self.sync_ha_chassis_group(context, network['id'], txn)
2038+
utils.sync_ha_chassis_group(
2039+
context, network['id'], self._nb_idl,
2040+
self._sb_idl, txn)
21092041

21102042
# Update the segment tags, if any
21112043
segments = segments_db.get_network_segments(context, network['id'])

neutron/tests/functional/common/ovn/test_utils.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414

15+
from unittest import mock
16+
17+
from oslo_utils import uuidutils
18+
1519
from neutron.common.ovn import constants as ovn_const
1620
from neutron.common.ovn import utils
1721
from neutron.tests.functional import base
@@ -58,3 +62,56 @@ def test_non_existing(self):
5862
self.assertEqual(directions[0], acl2.direction)
5963
self.assertEqual('drop', acl2.action)
6064
self.assertEqual(matches[0], acl2.match)
65+
66+
67+
class TestSyncHaChassisGroup(base.TestOVNFunctionalBase):
68+
69+
def test_sync_ha_chassis_group(self):
70+
plugin = mock.Mock()
71+
plugin.get_network.return_value = {}
72+
network_id = uuidutils.generate_uuid()
73+
hcg_name = utils.ovn_name(network_id)
74+
chassis1 = self.add_fake_chassis('host1', azs=[],
75+
enable_chassis_as_gw=True)
76+
chassis2 = self.add_fake_chassis('host2', azs=[],
77+
enable_chassis_as_gw=True)
78+
self.add_fake_chassis('host3')
79+
80+
with self.nb_api.transaction(check_error=True) as txn:
81+
utils.sync_ha_chassis_group(self.context, network_id, self.nb_api,
82+
self.sb_api, txn)
83+
84+
ha_chassis = self.nb_api.db_find('HA_Chassis').execute(
85+
check_error=True)
86+
ha_chassis_names = [hc['chassis_name'] for hc in ha_chassis]
87+
self.assertEqual(2, len(ha_chassis))
88+
self.assertEqual(sorted([chassis1, chassis2]),
89+
sorted(ha_chassis_names))
90+
91+
hcg = self.nb_api.ha_chassis_group_get(hcg_name).execute(
92+
check_error=True)
93+
self.assertEqual(hcg_name, hcg.name)
94+
ha_chassis_exp = sorted([str(hc['_uuid']) for hc in ha_chassis])
95+
ha_chassis_ret = sorted([str(hc.uuid) for hc in hcg.ha_chassis])
96+
self.assertEqual(ha_chassis_exp, ha_chassis_ret)
97+
98+
# Delete one GW chassis and resync the HA chassis group associated to
99+
# the same network. The method will now not create again the existing
100+
# HA Chassis Group register but will update the "ha_chassis" list.
101+
self.del_fake_chassis(chassis2)
102+
with self.nb_api.transaction(check_error=True) as txn:
103+
utils.sync_ha_chassis_group(self.context, network_id, self.nb_api,
104+
self.sb_api, txn)
105+
106+
ha_chassis = self.nb_api.db_find('HA_Chassis').execute(
107+
check_error=True)
108+
ha_chassis_names = [hc['chassis_name'] for hc in ha_chassis]
109+
self.assertEqual(1, len(ha_chassis))
110+
self.assertEqual([chassis1], ha_chassis_names)
111+
112+
hcg = self.nb_api.ha_chassis_group_get(hcg_name).execute(
113+
check_error=True)
114+
self.assertEqual(hcg_name, hcg.name)
115+
ha_chassis_exp = str(ha_chassis[0]['_uuid'])
116+
ha_chassis_ret = str(hcg.ha_chassis[0].uuid)
117+
self.assertEqual(ha_chassis_exp, ha_chassis_ret)

0 commit comments

Comments
 (0)