Merge "[OVN] Support address group for ovn driver"

This commit is contained in:
Zuul 2024-10-28 20:05:18 +00:00 committed by Gerrit Code Review
commit 1df5dbd9b6
15 changed files with 285 additions and 2 deletions

View File

@ -233,6 +233,16 @@ def acl_remote_group_id(r, ip_version):
return ' && %s.%s == $%s' % (ip_version, src_or_dst, addrset_name)
def acl_remote_address_group_id(r, ip_version):
if not r.get('remote_address_group_id'):
return ''
src_or_dst = 'src' if r['direction'] == const.INGRESS_DIRECTION else 'dst'
addrset_name = utils.ovn_ag_addrset_name(r['remote_address_group_id'],
ip_version)
return ' && %s.%s == $%s' % (ip_version, src_or_dst, addrset_name)
def _add_sg_rule_acl_for_port_group(port_group, stateful, r):
# Update the match based on which direction this rule is for (ingress
# or egress).
@ -248,6 +258,9 @@ def _add_sg_rule_acl_for_port_group(port_group, stateful, r):
# Update the match if remote group id was specified.
match += acl_remote_group_id(r, ip_version)
# Update the match if remote address group id was specified.
match += acl_remote_address_group_id(r, ip_version)
# Update the match for the protocol (tcp, udp, icmp) and port/type
# range if specified.
match += acl_protocol_and_ports(r, icmp)

View File

@ -56,6 +56,7 @@ OVN_PORT_BINDING_PROFILE = portbindings.PROFILE
OVN_HOST_ID_EXT_ID_KEY = 'neutron:host_id'
OVN_LRSR_EXT_ID_KEY = 'neutron:is_static_route'
OVN_FIP_DISTRIBUTED_KEY = 'neutron:fip-distributed'
OVN_ADDRESS_GROUP_ID_KEY = 'neutron:address_group_id'
MIGRATING_ATTR = 'migrating_to'
OVN_ROUTER_PORT_OPTION_KEYS = ['router-port', 'nat-addresses',
@ -263,6 +264,7 @@ TYPE_ROUTER_PORTS = 'router_ports'
TYPE_SECURITY_GROUPS = 'security_groups'
TYPE_FLOATINGIPS = 'floatingips'
TYPE_SUBNETS = 'subnets'
TYPE_ADDRESS_GROUPS = 'address_groups'
_TYPES_PRIORITY_ORDER = (
TYPE_NETWORKS,
@ -272,6 +274,7 @@ _TYPES_PRIORITY_ORDER = (
TYPE_PORTS,
TYPE_ROUTER_PORTS,
TYPE_FLOATINGIPS,
TYPE_ADDRESS_GROUPS,
TYPE_SECURITY_GROUP_RULES)
DB_CONSISTENCY_CHECK_INTERVAL = 300 # 5 minutes

View File

@ -261,6 +261,15 @@ def ovn_pg_addrset_name(sg_id, ip_version):
return ('pg-%s-%s' % (sg_id, ip_version)).replace('-', '_')
def ovn_ag_addrset_name(ag_id, ip_version):
# The name of the address set for the given address group id and ip
# version. The format is:
# ag-<address group uuid>-<ip version>
# with all '-' replaced with '_'. This replacement is necessary
# because OVN doesn't support '-' in an address set name.
return ('ag-%s-%s' % (ag_id, ip_version)).replace('-', '_')
def ovn_port_group_name(sg_id):
# The name of the port group for the given security group id.
# The format is: pg-<security group uuid>.
@ -549,6 +558,7 @@ def get_revision_number(resource, resource_type):
constants.TYPE_ROUTERS,
constants.TYPE_ROUTER_PORTS,
constants.TYPE_SECURITY_GROUPS,
constants.TYPE_ADDRESS_GROUPS,
constants.TYPE_FLOATINGIPS, constants.TYPE_SUBNETS):
return resource['revision_number']
else:

View File

@ -37,6 +37,7 @@ class AddressGroupDbMixin(ag_ext.AddressGroupPluginBase):
res = address_group.to_dict()
res['addresses'] = [str(addr_assoc['address'])
for addr_assoc in address_group['addresses']]
res['standard_attr_id'] = address_group.standard_attr_id
return db_utils.resource_fields(res, fields)
@staticmethod

View File

@ -44,6 +44,7 @@ TYPE_ROUTER_PORTS = 'router_ports'
TYPE_SECURITY_GROUPS = 'security_groups'
TYPE_FLOATINGIPS = 'floatingips'
TYPE_SUBNETS = 'subnets'
TYPE_ADDRESS_GROUPS = 'address_groups'
_TYPES_PRIORITY_ORDER = (
TYPE_NETWORKS,
@ -53,6 +54,7 @@ _TYPES_PRIORITY_ORDER = (
TYPE_PORTS,
TYPE_ROUTER_PORTS,
TYPE_FLOATINGIPS,
TYPE_ADDRESS_GROUPS,
TYPE_SECURITY_GROUP_RULES)
# The order in which the resources should be created or updated by the

View File

@ -253,7 +253,7 @@ class OVNMechanismDriver(api.MechanismDriver):
resources.SEGMENT,
events.AFTER_DELETE)
# Handle security group/rule notifications
# Handle security group/rule or address group notifications
if self.sg_enabled:
registry.subscribe(self._create_security_group_precommit,
resources.SECURITY_GROUP,
@ -279,6 +279,15 @@ class OVNMechanismDriver(api.MechanismDriver):
registry.subscribe(self._process_sg_rule_notification,
resources.SECURITY_GROUP_RULE,
events.BEFORE_DELETE)
registry.subscribe(self._process_ag_notification,
resources.ADDRESS_GROUP,
events.AFTER_CREATE)
registry.subscribe(self._process_ag_notification,
resources.ADDRESS_GROUP,
events.AFTER_UPDATE)
registry.subscribe(self._process_ag_notification,
resources.ADDRESS_GROUP,
events.AFTER_DELETE)
def _remove_node_from_hash_ring(self, *args, **kwargs):
# The node_uuid attribute will be empty for worker types
@ -488,6 +497,25 @@ class OVNMechanismDriver(api.MechanismDriver):
return True
return False
def _process_ag_notification(
self, resource, event, trigger, payload):
context = payload.context
address_group = payload.latest_state
address_group_id = payload.resource_id
if event == events.AFTER_CREATE:
ovn_revision_numbers_db.create_initial_revision(
context, address_group_id, ovn_const.TYPE_ADDRESS_GROUPS,
std_attr_id=address_group['standard_attr_id'])
self._ovn_client.create_address_group(
context, address_group)
elif event == events.AFTER_UPDATE:
self._ovn_client.update_address_group(
context, address_group)
elif event == events.AFTER_DELETE:
self._ovn_client.delete_address_group(
context,
address_group_id)
def _is_network_type_supported(self, network_type):
return (network_type in [const.TYPE_LOCAL,
const.TYPE_FLAT,

View File

@ -36,6 +36,7 @@ RESOURCE_TYPE_MAP = {
ovn_const.TYPE_ROUTER_PORTS: 'Logical_Router_Port',
ovn_const.TYPE_FLOATINGIPS: 'NAT',
ovn_const.TYPE_SUBNETS: 'DHCP_Options',
ovn_const.TYPE_ADDRESS_GROUPS: 'Address_Set',
}

View File

@ -859,6 +859,14 @@ class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend):
pg_name = utils.ovn_port_group_name(pg_name)
return self.lookup('Port_Group', pg_name, default=None)
def get_address_set(self, as_name):
if uuidutils.is_uuid_like(as_name):
as_name_v4 = utils.ovn_ag_addrset_name(as_name, 'ip4')
as_name_v6 = utils.ovn_ag_addrset_name(as_name, 'ip6')
return (self.lookup('Address_Set', as_name_v4, default=None),
self.lookup('Address_Set', as_name_v6, default=None))
return self.lookup('Address_Set', as_name, default=None), None
def get_sg_port_groups(self):
"""Returns OVN port groups used as Neutron Security Groups.

View File

@ -206,6 +206,13 @@ class DBInconsistenciesPeriodics(SchemaAwarePeriodicsBase):
'ovn_update': self._ovn_client.update_router,
'ovn_delete': self._ovn_client.delete_router,
},
ovn_const.TYPE_ADDRESS_GROUPS: {
'neutron_get': self._ovn_client._plugin.get_address_group,
'ovn_get': self._nb_idl.get_address_set,
'ovn_create': self._ovn_client.create_address_group,
'ovn_update': self._ovn_client.update_address_group,
'ovn_delete': self._ovn_client.delete_address_group,
},
ovn_const.TYPE_SECURITY_GROUPS: {
'neutron_get': self._ovn_client._plugin.get_security_group,
'ovn_get': self._nb_idl.get_port_group,
@ -275,6 +282,29 @@ class DBInconsistenciesPeriodics(SchemaAwarePeriodicsBase):
# supposed to be.
revision_numbers_db.bump_revision(context, n_obj,
row.resource_type)
elif row.resource_type == ovn_const.TYPE_ADDRESS_GROUPS:
need_bump = False
for obj in ovn_obj:
if not obj:
# NOTE(liushy): We create two Address_Sets for
# one Address_Group at one ovn_create func.
res_map['ovn_create'](context, n_obj)
need_bump = False
break
ext_ids = getattr(obj, 'external_ids', {})
ovn_revision = int(ext_ids.get(
ovn_const.OVN_REV_NUM_EXT_ID_KEY, -1))
# NOTE(liushy): We have created two Address_Sets
# for one Address_Group, and we update both of
# them at one ovn_update func.
if ovn_revision != n_obj['revision_number']:
res_map['ovn_update'](context, n_obj)
need_bump = False
break
need_bump = True
if need_bump:
revision_numbers_db.bump_revision(context, n_obj,
row.resource_type)
else:
ext_ids = getattr(ovn_obj, 'external_ids', {})
ovn_revision = int(ext_ids.get(

View File

@ -2574,6 +2574,84 @@ class OVNClient(object):
db_rev.delete_revision(
context, rule['id'], ovn_const.TYPE_SECURITY_GROUP_RULES)
def _checkout_ip_list(self, addresses):
"""Return address map for addresses.
This method will check out ipv4 and ipv6 address list from the
given address list.
Eg. if addresses = ["192.168.2.2/32", "2001:db8::/32"], it will
return {"4":["192.168.2.2/32"], "6":["2001:db8::/32"]}.
:param addresses: address list.
"""
if not addresses:
addresses = []
ip_addresses = [netaddr.IPNetwork(ip)
for ip in addresses]
addr_map = {const.IP_VERSION_4: [], const.IP_VERSION_6: []}
for addr in ip_addresses:
addr_map[addr.version].append(str(addr.cidr))
return addr_map
def create_address_group(self, context, address_group):
addr_map_all = self._checkout_ip_list(
address_group.get('addresses'))
external_ids = {ovn_const.OVN_ADDRESS_GROUP_ID_KEY:
address_group['id'],
ovn_const.OVN_REV_NUM_EXT_ID_KEY: str(
utils.get_revision_number(
address_group,
ovn_const.TYPE_ADDRESS_GROUPS))
}
attrs = [('external_ids', external_ids),]
for ip_version in const.IP_ALLOWED_VERSIONS:
as_name = utils.ovn_ag_addrset_name(address_group['id'],
'ip' + str(ip_version))
with self._nb_idl.transaction(check_error=True) as txn:
txn.add(self._nb_idl.address_set_add(
as_name, addresses=addr_map_all[ip_version],
may_exist=True))
txn.add(self._nb_idl.db_set(
'Address_Set', as_name, *attrs))
db_rev.bump_revision(
context, address_group, ovn_const.TYPE_ADDRESS_GROUPS)
def update_address_group(self, context, address_group):
addr_map_db = self._checkout_ip_list(address_group['addresses'])
for ip_version in const.IP_ALLOWED_VERSIONS:
as_name = utils.ovn_ag_addrset_name(address_group['id'],
'ip' + str(ip_version))
check_rev_cmd = self._nb_idl.check_revision_number(
as_name, address_group, ovn_const.TYPE_ADDRESS_GROUPS)
with self._nb_idl.transaction(check_error=True) as txn:
txn.add(check_rev_cmd)
# For add/remove addresses
addr_ovn = self._nb_idl.get_address_set(as_name)[0].addresses
added = set(addr_map_db[ip_version]) - set(addr_ovn)
removed = set(addr_ovn) - set(addr_map_db[ip_version])
txn.add(self._nb_idl.address_set_add_addresses(
as_name,
added
))
txn.add(self._nb_idl.address_set_remove_addresses(
as_name,
removed
))
if check_rev_cmd.result == ovn_const.TXN_COMMITTED:
db_rev.bump_revision(
context, address_group, ovn_const.TYPE_ADDRESS_GROUPS)
def delete_address_group(self, context, address_group_id):
ipv4_as_name = utils.ovn_ag_addrset_name(address_group_id, 'ip4')
ipv6_as_name = utils.ovn_ag_addrset_name(address_group_id, 'ip6')
with self._nb_idl.transaction(check_error=True) as txn:
txn.add(self._nb_idl.address_set_del(
ipv4_as_name, if_exists=True))
txn.add(self._nb_idl.address_set_del(
ipv6_as_name, if_exists=True))
db_rev.delete_revision(
context, address_group_id, ovn_const.TYPE_ADDRESS_GROUPS)
def _find_metadata_port(self, context, network_id):
if not ovn_conf.is_ovn_metadata_enabled():
return

View File

@ -180,3 +180,44 @@ class TestOVNClient(base.TestOVNFunctionalBase,
def test_router_reside_chassis_redirect_non_dvr_geneve_net(self):
self._test_router_reside_chassis_redirect(False, 'geneve')
def test_process_address_group(self):
def _find_address_set_for_ag():
as_v4 = self.nb_api.lookup(
'Address_Set',
ovn_utils.ovn_ag_addrset_name(
ag['id'], 'ip' + str(constants.IP_VERSION_4)),
default=None)
as_v6 = self.nb_api.lookup(
'Address_Set',
ovn_utils.ovn_ag_addrset_name(
ag['id'], 'ip' + str(constants.IP_VERSION_6)),
default=None)
return as_v4, as_v6
ovn_client = self.mech_driver._ovn_client
ag_args = {'project_id': 'project_1',
'name': 'test_address_group',
'description': 'test address group',
'addresses': ['192.168.2.2/32',
'2001:db8::/32']}
ag = self.plugin.create_address_group(self.context,
{'address_group': ag_args})
self.assertIsNotNone(_find_address_set_for_ag()[0])
self.assertIsNotNone(_find_address_set_for_ag()[1])
# Call the create_address_group again to ensure that the create
# command automatically checks for existing Address_Set
ovn_client.create_address_group(self.context, ag)
# Update the address group
ag['addresses'] = ['20.0.0.1/32', '2002:db8::/32']
ovn_client.update_address_group(self.context, ag)
as_v4_new = _find_address_set_for_ag()[0]
as_v6_new = _find_address_set_for_ag()[1]
self.assertEqual(['20.0.0.1/32'], as_v4_new.addresses)
self.assertEqual(['2002:db8::/32'], as_v6_new.addresses)
# Delete the address group
ovn_client.delete_address_group(self.context, ag['id'])
self.assertEqual((None, None), _find_address_set_for_ag())

View File

@ -29,6 +29,7 @@ from neutron.db import ovn_revision_numbers_db as ovn_rn_db
import neutron.extensions
from neutron.services.revisions import revision_plugin
from neutron.tests.unit.db import test_db_base_plugin_v2
from neutron.tests.unit.extensions import test_address_group
from neutron.tests.unit.extensions import test_l3
from neutron.tests.unit.extensions import test_securitygroup
@ -150,6 +151,7 @@ class TestExtensionManager(extensions.PluginAwareExtensionManager):
class TestRevisionNumberMaintenance(test_securitygroup.SecurityGroupsTestCase,
test_address_group.AddressGroupTestCase,
test_l3.L3NatTestCaseMixin):
def setUp(self):
@ -246,6 +248,9 @@ class TestRevisionNumberMaintenance(test_securitygroup.SecurityGroupsTestCase,
sg['id'], 'ingress', n_const.PROTO_NUM_TCP)
sg_rule = self._make_security_group_rule(
self.fmt, rule)['security_group_rule']
ag = self.deserialize(
self.fmt, self._create_address_group(
**{'name': 'ag1'}))['address_group']
self._create_initial_revision(router['id'], ovn_rn_db.TYPE_ROUTERS)
self._create_initial_revision(subnet['id'], ovn_rn_db.TYPE_SUBNETS)
@ -256,6 +261,7 @@ class TestRevisionNumberMaintenance(test_securitygroup.SecurityGroupsTestCase,
self._create_initial_revision(sg_rule['id'],
ovn_rn_db.TYPE_SECURITY_GROUP_RULES)
self._create_initial_revision(self.net['id'], ovn_rn_db.TYPE_NETWORKS)
self._create_initial_revision(ag['id'], ovn_rn_db.TYPE_ADDRESS_GROUPS)
if delete:
self._delete('security-group-rules', sg_rule['id'])
@ -265,6 +271,7 @@ class TestRevisionNumberMaintenance(test_securitygroup.SecurityGroupsTestCase,
self._delete('routers', router['id'])
self._delete('subnets', subnet['id'])
self._delete('networks', self.net['id'])
self._delete('address-groups', ag['id'])
def test_get_inconsistent_resources_order(self):
self._prepare_resources_for_ordering_test()

View File

@ -30,6 +30,7 @@ from neutron.db.models import ovn as ovn_models
from neutron.db import ovn_revision_numbers_db
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import maintenance
from neutron.tests import base
from neutron.tests.unit.extensions import test_address_group as test_ag
from neutron.tests.unit import fake_resources as fakes
from neutron.tests.unit.plugins.ml2 import test_security_group as test_sg
from neutron.tests.unit import testlib_api
@ -139,7 +140,8 @@ class TestSchemaAwarePeriodicsBase(testlib_api.SqlTestCaseLight):
@mock.patch.object(maintenance.DBInconsistenciesPeriodics,
'has_lock', mock.PropertyMock(return_value=True))
class TestDBInconsistenciesPeriodics(testlib_api.SqlTestCaseLight,
test_sg.Ml2SecurityGroupsTestCase):
test_sg.Ml2SecurityGroupsTestCase,
test_ag.AddressGroupTestCase):
def setUp(self):
ovn_conf.register_opts()
@ -148,6 +150,9 @@ class TestDBInconsistenciesPeriodics(testlib_api.SqlTestCaseLight,
self.fmt, name='net1', admin_state_up=True)['network']
self.port = self._make_port(
self.fmt, self.net['id'], name='port1')['port']
self.ag = self.deserialize(
self.fmt, self._create_address_group(
**{'name': 'ag1'}))['address_group']
self.fake_ovn_client = mock.MagicMock()
self.periodic = maintenance.DBInconsistenciesPeriodics(
self.fake_ovn_client)
@ -286,6 +291,52 @@ class TestDBInconsistenciesPeriodics(testlib_api.SqlTestCaseLight,
def test_fix_security_group_create_version_mismatch(self):
self._test_fix_security_group_create(revision_number=2)
def _test_fix_create_update_address_group(self, ovn_rev, neutron_rev):
_nb_idl = self.fake_ovn_client._nb_idl
with db_api.CONTEXT_WRITER.using(self.ctx):
self.ag['revision_number'] = neutron_rev
# Create an entry to the revision_numbers table and assert the
# initial revision_number for our test object is the expected
ovn_revision_numbers_db.create_initial_revision(
self.ctx, self.ag['id'], constants.TYPE_ADDRESS_GROUPS,
revision_number=ovn_rev)
row = ovn_revision_numbers_db.get_revision_row(self.ctx,
self.ag['id'])
self.assertEqual(ovn_rev, row.revision_number)
if ovn_rev < 0:
_nb_idl.get_address_set.return_value = None, None
else:
fake_as_v4 = mock.Mock(external_ids={
constants.OVN_REV_NUM_EXT_ID_KEY: ovn_rev})
fake_as_v6 = mock.Mock(external_ids={
constants.OVN_REV_NUM_EXT_ID_KEY: ovn_rev})
_nb_idl.get_address_set.return_value = fake_as_v4, fake_as_v6
self.fake_ovn_client._plugin.get_address_group.return_value = \
self.ag
self.periodic._fix_create_update(self.ctx, row)
# Since the revision number was < 0, make sure
# create_address_group() is invoked with the latest
# version of the object in the neutron database
if ovn_rev < 0:
self.fake_ovn_client.create_address_group.\
assert_called_once_with(self.ctx, self.ag)
# If the revision number is > 0 it means that the object already
# exist and we just need to update to match the latest in the
# neutron database so, update_address_group() should be called.
else:
self.fake_ovn_client.update_address_group.\
assert_called_once_with(self.ctx, self.ag)
def test_fix_address_group_create(self):
self._test_fix_create_update_address_group(ovn_rev=-1, neutron_rev=2)
def test_fix_address_group_update(self):
self._test_fix_create_update_address_group(ovn_rev=5, neutron_rev=7)
@mock.patch.object(maintenance, 'LOG')
def test__fix_create_update_no_sttd_attr(self, mock_log):
row_net = ovn_models.OVNRevisionNumbers(

View File

@ -237,6 +237,12 @@ class TestOVNClient(TestOVNClientBase):
self.ovn_client._add_router_ext_gw(mock.Mock(), router, txn))
self.nb_idl.add_static_route.assert_not_called()
def test_checkout_ip_list(self):
addresses = ["192.168.2.2/32", "2001:db8::/32"]
add_map = self.ovn_client._checkout_ip_list(addresses)
self.assertEqual(["192.168.2.2/32"], add_map[const.IP_VERSION_4])
self.assertEqual(["2001:db8::/32"], add_map[const.IP_VERSION_6])
def test_update_lsp_host_info_up(self):
context = mock.MagicMock()
host_id = 'fake-binding-host-id'

View File

@ -0,0 +1,4 @@
---
features:
- |
Add support for the ``address-group`` in the OVN mechanism driver.