Browse Source

Merge "Add support for virtual port type"

tags/neutron-merge
Zuul 2 weeks ago
parent
commit
635cd5e10f
10 changed files with 580 additions and 23 deletions
  1. +3
    -0
      networking_ovn/common/constants.py
  2. +104
    -23
      networking_ovn/common/ovn_client.py
  3. +14
    -0
      networking_ovn/common/utils.py
  4. +73
    -0
      networking_ovn/ovsdb/commands.py
  5. +10
    -0
      networking_ovn/ovsdb/impl_idl_ovn.py
  6. +36
    -0
      networking_ovn/ovsdb/ovn_api.py
  7. +238
    -0
      networking_ovn/tests/functional/test_mech_driver.py
  8. +5
    -0
      networking_ovn/tests/unit/fakes.py
  9. +88
    -0
      networking_ovn/tests/unit/ml2/test_mech_driver.py
  10. +9
    -0
      releasenotes/notes/virtual-ports-fe725a817ce45e6d.yaml

+ 3
- 0
networking_ovn/common/constants.py View File

@@ -173,3 +173,6 @@ UNKNOWN_ADDR = 'unknown'

# TODO(lucasagomes): Create constants for other LSP types
LSP_TYPE_LOCALNET = 'localnet'
LSP_TYPE_VIRTUAL = 'virtual'
LSP_OPTIONS_VIRTUAL_PARENTS_KEY = 'virtual-parents'
LSP_OPTIONS_VIRTUAL_IP_KEY = 'virtual-ip'

+ 104
- 23
networking_ovn/common/ovn_client.py View File

@@ -92,6 +92,11 @@ class OVNClient(object):
for cmd in commands:
txn.add(cmd)

def _is_virtual_port_supported(self):
# TODO(lucasagomes): Remove this method in the future. The
# "virtual" port type was added in the version 2.12 of OVN
return self._sb_idl.is_col_present('Port_Binding', 'virtual_parent')

def _get_allowed_addresses_from_port(self, port):
if not port.get(psec.PORTSECURITY):
return [], []
@@ -191,7 +196,14 @@ class OVNClient(object):
external_ids=subnet_dhcp_options['external_ids'])
return {'cmd': add_dhcp_opts_cmd}

def get_virtual_port_parents(self, virtual_ip, port):
ls = self._nb_idl.ls_get(utils.ovn_name(port['network_id'])).execute(
check_error=True)
return [lsp.name for lsp in ls.ports for ps in lsp.port_security
if lsp.name != port['id'] and virtual_ip in ps]

def _get_port_options(self, port, qos_options=None):
context = n_context.get_admin_context()
binding_prof = utils.validate_and_get_data_from_binding_profile(port)
if qos_options is None:
qos_options = self._qos_driver.get_qos_options(port)
@@ -215,13 +227,24 @@ class OVNClient(object):
address = port['mac_address']
for ip in port.get('fixed_ips', []):
try:
subnet = self._plugin.get_subnet(
n_context.get_admin_context(), ip['subnet_id'])
subnet = self._plugin.get_subnet(context, ip['subnet_id'])
except n_exc.SubnetNotFound:
continue
address += ' ' + ip['ip_address']
ip_addr = ip['ip_address']
address += ' ' + ip_addr
cidrs += ' {}/{}'.format(ip['ip_address'],
subnet['cidr'].split('/')[1])

# Check if the port being created is a virtual port
if (self._is_virtual_port_supported() and
not port['device_owner']):
parents = self.get_virtual_port_parents(ip_addr, port)
if parents:
port_type = ovn_const.LSP_TYPE_VIRTUAL
options[ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY] = ip_addr
options[ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY] = (
','.join(parents))

port_security, new_macs = (
self._get_allowed_addresses_from_port(port))
addresses = [address]
@@ -318,15 +341,29 @@ class OVNClient(object):
'dhcpv6_options': dhcpv6_options
}

# TODO(lucasgomes): Remove this workaround in the future,
# the core OVN version >= 2.12 supports the "virtual" port
# type which deals with these situations.
# NOTE(mjozefcz): Do not set addresses if the port is not
# bound and has no device_owner - possibly it is a VirtualIP
# port used for Octavia (VRRP).
# For more details check related bug #1789686.
if (not port.get('device_owner') and
if (not self._is_virtual_port_supported() and
not port.get('device_owner') and
port.get(portbindings.VIF_TYPE) ==
portbindings.VIF_TYPE_UNBOUND):
kwargs['addresses'] = []

# Check if the parent port was created with the
# allowed_address_pairs already set
allowed_address_pairs = port.get('allowed_address_pairs', [])
if (self._is_virtual_port_supported() and
allowed_address_pairs and
port_info.type != ovn_const.LSP_TYPE_VIRTUAL):
addrs = [addr['ip_address'] for addr in allowed_address_pairs]
self._set_unset_virtual_port_type(
admin_context, txn, port, addrs)

port_cmd = txn.add(self._nb_idl.create_lswitch_port(
**kwargs))

@@ -386,6 +423,29 @@ class OVNClient(object):
port_object, skip_trusted_port=skip_trusted_port)
return []

def _set_unset_virtual_port_type(self, context, txn, parent_port,
addresses, unset=False):
cmd = self._nb_idl.set_lswitch_port_to_virtual_type
if unset:
cmd = self._nb_idl.unset_lswitch_port_to_virtual_type

for addr in addresses:
virt_port = self._plugin.get_ports(context, filters={
portbindings.VIF_TYPE: portbindings.VIF_TYPE_UNBOUND,
'network_id': [parent_port['network_id']],
'fixed_ips': {'ip_address': [addr]}})
if not virt_port:
continue
virt_port = virt_port[0]
args = {'lport_name': virt_port['id'],
'virtual_parent': parent_port['id'],
'if_exists': True}
LOG.debug("Parent port %(virtual_parent)s found for "
"virtual port %(lport_name)s", args)
if not unset:
args['vip'] = addr
txn.add(cmd(**args))

# TODO(lucasagomes): The ``port_object`` parameter was added to
# keep things backward compatible. Remove it in the Rocky release.
def update_port(self, port, qos_options=None, port_object=None):
@@ -434,15 +494,30 @@ class OVNClient(object):
else:
dhcpv6_options = [port_info.dhcpv6_options['uuid']]

# TODO(lucasgomes): Remove this workaround in the future,
# the core OVN version >= 2.12 supports the "virtual" port
# type which deals with these situations.
# NOTE(mjozefcz): Do not set addresses if the port is not
# bound and has no device_owner - possibly it is a VirtualIP
# port used for Octavia (VRRP).
# For more details check related bug #1789686.
if (not port.get('device_owner') and
if (not self._is_virtual_port_supported() and
not port.get('device_owner') and
port.get(portbindings.VIF_TYPE) ==
portbindings.VIF_TYPE_UNBOUND):
columns_dict['addresses'] = []

ovn_port = self._nb_idl.lookup('Logical_Switch_Port', port['id'])
addr_pairs_diff = utils.compute_address_pairs_diff(ovn_port, port)

if (self._is_virtual_port_supported() and
port_info.type != ovn_const.LSP_TYPE_VIRTUAL):
self._set_unset_virtual_port_type(
admin_context, txn, port, addr_pairs_diff.added)
self._set_unset_virtual_port_type(
admin_context, txn, port, addr_pairs_diff.removed,
unset=True)

# NOTE(lizk): Fail port updating if port doesn't exist. This
# prevents any new inserted resources to be orphan, such as port
# dhcp options or ACL rules for port, e.g. a port was created
@@ -461,29 +536,12 @@ class OVNClient(object):
if_exists=False,
**columns_dict))

ovn_port = self._nb_idl.lookup('Logical_Switch_Port', port['id'])
# Determine if security groups or fixed IPs are updated.
old_sg_ids = set(self._get_lsp_backward_compat_sgs(
ovn_port, port_object=port_object))
new_sg_ids = set(utils.get_lsp_security_groups(port))
detached_sg_ids = old_sg_ids - new_sg_ids
attached_sg_ids = new_sg_ids - old_sg_ids
old_fixed_ips = utils.remove_macs_from_lsp_addresses(
ovn_port.addresses)
new_fixed_ips = [x['ip_address'] for x in
port.get('fixed_ips', [])]
old_allowed_address_pairs = (
utils.get_allowed_address_pairs_ip_addresses_from_ovn_port(
ovn_port))
new_allowed_address_pairs = (
utils.get_allowed_address_pairs_ip_addresses(port))
is_fixed_ips_updated = (
sorted(old_fixed_ips) != sorted(new_fixed_ips))
is_allowed_ips_updated = (sorted(old_allowed_address_pairs) !=
sorted(new_allowed_address_pairs))

port_security_changed = utils.is_port_security_enabled(port) != (
bool(ovn_port.port_security))

if self._nb_idl.is_port_groups_supported():
for sg in detached_sg_ids:
@@ -503,6 +561,16 @@ class OVNClient(object):
utils.is_lsp_trusted(port)):
self._del_port_from_drop_port_group(port['id'], txn)
else:

old_fixed_ips = utils.remove_macs_from_lsp_addresses(
ovn_port.addresses)
new_fixed_ips = [x['ip_address'] for x in
port.get('fixed_ips', [])]
is_fixed_ips_updated = (
sorted(old_fixed_ips) != sorted(new_fixed_ips))
port_security_changed = (
utils.is_port_security_enabled(port) !=
bool(ovn_port.port_security))
# Refresh ACLs for changed security groups or fixed IPs.
if (detached_sg_ids or attached_sg_ids or
is_fixed_ips_updated or port_security_changed):
@@ -545,7 +613,7 @@ class OVNClient(object):
addrs_add=None,
addrs_remove=addresses_old[ip_version]))

if is_fixed_ips_updated or is_allowed_ips_updated:
if is_fixed_ips_updated or addr_pairs_diff.changed:
# We have refreshed address sets for attached and
# detached security groups, so now we only need to take
# care of unchanged security groups.
@@ -609,6 +677,19 @@ class OVNClient(object):
if port_object and self.is_dns_required_for_port(port_object):
self.add_txns_to_remove_port_dns_records(txn, port_object)

# Check if the port being deleted is a virtual parent
if (ovn_port.type != ovn_const.LSP_TYPE_VIRTUAL and
self._is_virtual_port_supported()):
ls = self._nb_idl.ls_get(network_id).execute(
check_error=True)
cmd = self._nb_idl.unset_lswitch_port_to_virtual_type
for lsp in ls.ports:
if lsp.type != ovn_const.LSP_TYPE_VIRTUAL:
continue
if port_id in lsp.options.get(
ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY, ''):
txn.add(cmd(lsp.name, port_id, if_exists=True))

# TODO(lucasagomes): The ``port_object`` parameter was added to
# keep things backward compatible. Remove it in the Rocky release.
def delete_port(self, port_id, port_object=None):

+ 14
- 0
networking_ovn/common/utils.py View File

@@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.

import collections
import inspect
import os
import re
@@ -37,6 +38,9 @@ from networking_ovn.common import exceptions as ovn_exc

DNS_RESOLVER_FILE = "/etc/resolv.conf"

AddrPairsDiff = collections.namedtuple(
'AddrPairsDiff', ['added', 'removed', 'changed'])


def ovn_name(id):
# The name of the OVN entry will be neutron-<UUID>
@@ -430,3 +434,13 @@ def is_neutron_dhcp_agent_port(port):
return (port['device_owner'] == const.DEVICE_OWNER_DHCP and
(port['device_id'] == const.DEVICE_ID_RESERVED_DHCP_PORT or
port['device_id'].startswith('dhcp')))


def compute_address_pairs_diff(ovn_port, neutron_port):
"""Compute the differences in the allowed_address_pairs field."""
ovn_ap = get_allowed_address_pairs_ip_addresses_from_ovn_port(
ovn_port)
neutron_ap = get_allowed_address_pairs_ip_addresses(neutron_port)
added = set(neutron_ap) - set(ovn_ap)
removed = set(ovn_ap) - set(neutron_ap)
return AddrPairsDiff(added, removed, changed=any(added or removed))

+ 73
- 0
networking_ovn/ovsdb/commands.py View File

@@ -1103,3 +1103,76 @@ class DeleteLRouterExtGwCommand(command.BaseCommand):
return

_delvalue_from_list(lrouter, 'ports', lrouter_port)


class SetLSwitchPortToVirtualTypeCommand(command.BaseCommand):
def __init__(self, api, lport, vip, parent, if_exists):
super(SetLSwitchPortToVirtualTypeCommand, self).__init__(api)
self.lport = lport
self.vip = vip
self.parent = parent
self.if_exists = if_exists

def run_idl(self, txn):
try:
lsp = idlutils.row_by_value(self.api.idl, 'Logical_Switch_Port',
'name', self.lport)
except idlutils.RowNotFound:
if self.if_exists:
return
msg = "Logical Switch Port %s does not exist" % self.lport
raise RuntimeError(msg)

options = lsp.options
options[ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY] = self.vip
virtual_parents = options.get(
ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY, set())
if virtual_parents:
virtual_parents = set(virtual_parents.split(','))

virtual_parents.add(self.parent)
options[ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY] = ','.join(
virtual_parents)
setattr(lsp, 'options', options)
setattr(lsp, 'type', ovn_const.LSP_TYPE_VIRTUAL)


class UnsetLSwitchPortToVirtualTypeCommand(command.BaseCommand):
def __init__(self, api, lport, parent, if_exists):
super(UnsetLSwitchPortToVirtualTypeCommand, self).__init__(api)
self.lport = lport
self.parent = parent
self.if_exists = if_exists

def run_idl(self, txn):
try:
lsp = idlutils.row_by_value(self.api.idl, 'Logical_Switch_Port',
'name', self.lport)
except idlutils.RowNotFound:
if self.if_exists:
return
msg = "Logical Switch Port %s does not exist" % self.lport
raise RuntimeError(msg)

options = lsp.options
virtual_parents = options.get(
ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY, set())
if virtual_parents:
virtual_parents = set(virtual_parents.split(','))

try:
virtual_parents.remove(self.parent)
except KeyError:
pass

# If virtual-parents is now empty, change the type and remove the
# virtual-parents and virtual-ip options
if not virtual_parents:
options.pop(ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY, None)
options.pop(ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY, None)
setattr(lsp, 'type', '')
else:
options[ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY] = ','.join(
virtual_parents)

setattr(lsp, 'options', options)

+ 10
- 0
networking_ovn/ovsdb/impl_idl_ovn.py View File

@@ -710,6 +710,16 @@ class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend):
def check_liveness(self):
return cmd.CheckLivenessCommand(self)

def set_lswitch_port_to_virtual_type(self, lport_name, vip,
virtual_parent, if_exists=True):
return cmd.SetLSwitchPortToVirtualTypeCommand(
self, lport_name, vip, virtual_parent, if_exists)

def unset_lswitch_port_to_virtual_type(self, lport_name,
virtual_parent, if_exists=True):
return cmd.UnsetLSwitchPortToVirtualTypeCommand(
self, lport_name, virtual_parent, if_exists)


class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend):
def __init__(self, connection):

+ 36
- 0
networking_ovn/ovsdb/ovn_api.py View File

@@ -601,6 +601,42 @@ class API(api.API):
:returns: The Address Set row or None
"""

@abc.abstractmethod
def set_lswitch_port_to_virtual_type(self, lport_name, vip,
virtual_parent, if_exists=True):
"""Set the type of a given port to "virtual".

Set the type of a given port to "virtual" and all its related
options.

:param lport_name: The name of the lport
:type lport_name: string
:param vip: The virtual ip
:type vip: string
:param virtual_parent: The name of the parent lport
:type virtual_parent: string
:param if_exists: Do not fail if lport does not exist
:type if_exists: bool
:returns: :class:`Command` with no result
"""

@abc.abstractmethod
def unset_lswitch_port_to_virtual_type(self, lport_name,
virtual_parent, if_exists=True):
"""Unset the type of a given port from "virtual".

Unset the type of a given port from "virtual" and all its related
options.

:param lport_name: The name of the lport
:type lport_name: string
:param virtual_parent: The name of the parent lport
:type virtual_parent: string
:param if_exists: Do not fail if lport does not exist
:type if_exists: bool
:returns: :class:`Command` with no result
"""


@six.add_metaclass(abc.ABCMeta)
class SbAPI(api.API):

+ 238
- 0
networking_ovn/tests/functional/test_mech_driver.py View File

@@ -12,9 +12,12 @@
# License for the specific language governing permissions and limitations
# under the License.

import mock

from oslo_config import cfg
from oslo_utils import uuidutils

from networking_ovn.common import constants as ovn_const
from networking_ovn.common import utils
from networking_ovn.db import revision as db_rev
from networking_ovn.tests.functional import base
@@ -175,3 +178,238 @@ class TestNetworkMTUUpdate(base.TestOVNFunctionalBase):
self.assertEqual(
base_revision.updated_at,
second_revision.updated_at)

@mock.patch('networking_ovn.common.ovn_client.OVNClient'
'._is_virtual_port_supported', lambda *args: True)
class TestVirtualPorts(base.TestOVNFunctionalBase):

def setUp(self):
super(TestVirtualPorts, self).setUp()
self._ovn_client = self.mech_driver._ovn_client
self.n1 = self._make_network(self.fmt, 'n1', True)
res = self._create_subnet(self.fmt, self.n1['network']['id'],
'10.0.0.0/24')
self.sub = self.deserialize(self.fmt, res)

def _create_port(self, fixed_ip=None, allowed_address=None):
port_data = {
'port': {'network_id': self.n1['network']['id'],
'tenant_id': self._tenant_id}}
if fixed_ip:
port_data['port']['fixed_ips'] = [{'ip_address': fixed_ip}]

if allowed_address:
port_data['port']['allowed_address_pairs'] = [
{'ip_address': allowed_address}]

port_req = self.new_create_request('ports', port_data, self.fmt)
port_res = port_req.get_response(self.api)
self.assertEqual(201, port_res.status_int)
return self.deserialize(self.fmt, port_res)['port']

def _update_allowed_address_pair(self, port_id, data):
port_data = {
'port': {'allowed_address_pairs': data}}
port_req = self.new_update_request('ports', port_data, port_id,
self.fmt)
port_res = port_req.get_response(self.api)
self.assertEqual(200, port_res.status_int)
return self.deserialize(self.fmt, port_res)['port']

def _set_allowed_address_pair(self, port_id, ip):
return self._update_allowed_address_pair(port_id, [{'ip_address': ip}])

def _unset_allowed_address_pair(self, port_id):
return self._update_allowed_address_pair(port_id, [])

def _find_port_row(self, port_id):
for row in self.nb_api._tables['Logical_Switch_Port'].rows.values():
if row.name == port_id:
return row

def test_virtual_port_created_before(self):
virt_port = self._create_port()
virt_ip = virt_port['fixed_ips'][0]['ip_address']

# Create the master port with the VIP address already set in
# the allowed_address_pairs field
master = self._create_port(allowed_address=virt_ip)

# Assert the virt port has the type virtual and master is set
# as parent
ovn_vport = self._find_port_row(virt_port['id'])
self.assertEqual(ovn_const.LSP_TYPE_VIRTUAL, ovn_vport.type)
self.assertEqual(
virt_ip,
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY])
self.assertEqual(
master['id'],
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY])

# Create the backport parent port
backup = self._create_port(allowed_address=virt_ip)

# Assert the virt port now also includes the backup port as a parent
ovn_vport = self._find_port_row(virt_port['id'])
self.assertEqual(ovn_const.LSP_TYPE_VIRTUAL, ovn_vport.type)
self.assertEqual(
virt_ip,
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY])
self.assertIn(
master['id'],
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY])
self.assertIn(
backup['id'],
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY])

def test_virtual_port_update_address_pairs(self):
master = self._create_port()
backup = self._create_port()
virt_port = self._create_port()
virt_ip = virt_port['fixed_ips'][0]['ip_address']

# Assert the virt port does not yet have the type virtual (no
# address pairs were set yet)
ovn_vport = self._find_port_row(virt_port['id'])
self.assertEqual("", ovn_vport.type)
self.assertNotIn(ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY,
ovn_vport.options)
self.assertNotIn(ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY,
ovn_vport.options)

# Set the virt IP to the allowed address pairs of the master port
self._set_allowed_address_pair(master['id'], virt_ip)

# Assert the virt port is now updated
ovn_vport = self._find_port_row(virt_port['id'])
self.assertEqual(ovn_const.LSP_TYPE_VIRTUAL, ovn_vport.type)
self.assertEqual(
virt_ip,
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY])
self.assertEqual(
master['id'],
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY])

# Set the virt IP to the allowed address pairs of the backup port
self._set_allowed_address_pair(backup['id'], virt_ip)

# Assert the virt port now includes the backup port as a parent
ovn_vport = self._find_port_row(virt_port['id'])
self.assertEqual(ovn_const.LSP_TYPE_VIRTUAL, ovn_vport.type)
self.assertEqual(
virt_ip,
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY])
self.assertIn(
master['id'],
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY])
self.assertIn(
backup['id'],
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY])

# Remove the address pairs from the master port
self._unset_allowed_address_pair(master['id'])

# Assert the virt port now only has the backup port as a parent
ovn_vport = self._find_port_row(virt_port['id'])
self.assertEqual(ovn_const.LSP_TYPE_VIRTUAL, ovn_vport.type)
self.assertEqual(
virt_ip,
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY])
self.assertEqual(
backup['id'],
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY])

# Remove the address pairs from the backup port
self._unset_allowed_address_pair(backup['id'])

# Assert the virt port is not type virtual anymore and the virtual
# port options are cleared
ovn_vport = self._find_port_row(virt_port['id'])
self.assertEqual("", ovn_vport.type)
self.assertNotIn(ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY,
ovn_vport.options)
self.assertNotIn(ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY,
ovn_vport.options)

def test_virtual_port_created_after(self):
master = self._create_port(fixed_ip='10.0.0.11')
backup = self._create_port(fixed_ip='10.0.0.12')
virt_ip = '10.0.0.55'

# Set the virt IP to the master and backup ports *before* creating
# the virtual port
self._set_allowed_address_pair(master['id'], virt_ip)
self._set_allowed_address_pair(backup['id'], virt_ip)

virt_port = self._create_port(fixed_ip=virt_ip)

# Assert the virtual port has been created with the
# right type and parents
ovn_vport = self._find_port_row(virt_port['id'])
self.assertEqual(ovn_const.LSP_TYPE_VIRTUAL, ovn_vport.type)
self.assertEqual(
virt_ip,
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY])
self.assertIn(
master['id'],
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY])
self.assertIn(
backup['id'],
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY])

def test_virtual_port_delete_parents(self):
master = self._create_port()
backup = self._create_port()
virt_port = self._create_port()
virt_ip = virt_port['fixed_ips'][0]['ip_address']

# Assert the virt port does not yet have the type virtual (no
# address pairs were set yet)
ovn_vport = self._find_port_row(virt_port['id'])
self.assertEqual("", ovn_vport.type)
self.assertNotIn(ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY,
ovn_vport.options)
self.assertNotIn(ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY,
ovn_vport.options)

# Set allowed address paris to the master and backup ports
self._set_allowed_address_pair(master['id'], virt_ip)
self._set_allowed_address_pair(backup['id'], virt_ip)

# Assert the virtual port is correct
ovn_vport = self._find_port_row(virt_port['id'])
self.assertEqual(ovn_const.LSP_TYPE_VIRTUAL, ovn_vport.type)
self.assertEqual(
virt_ip,
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY])
self.assertIn(
master['id'],
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY])
self.assertIn(
backup['id'],
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY])

# Delete the backup port
self._delete('ports', backup['id'])

# Assert the virt port now only has the master port as a parent
ovn_vport = self._find_port_row(virt_port['id'])
self.assertEqual(ovn_const.LSP_TYPE_VIRTUAL, ovn_vport.type)
self.assertEqual(
virt_ip,
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY])
self.assertEqual(
master['id'],
ovn_vport.options[ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY])

# Delete the master port
self._delete('ports', master['id'])

# Assert the virt port is not type virtual anymore and the virtual
# port options are cleared
ovn_vport = self._find_port_row(virt_port['id'])
self.assertEqual("", ovn_vport.type)
self.assertNotIn(ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY,
ovn_vport.options)
self.assertNotIn(ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY,
ovn_vport.options)

+ 5
- 0
networking_ovn/tests/unit/fakes.py View File

@@ -141,6 +141,9 @@ class FakeOvsdbNbOvnIdl(object):
self.db_set = mock.Mock()
self.db_clear = mock.Mock()
self.db_remove = mock.Mock()
self.set_lswitch_port_to_virtual_type = mock.Mock()
self.unset_lswitch_port_to_virtual_type = mock.Mock()
self.ls_get = mock.Mock()


class FakeOvsdbSbOvnIdl(object):
@@ -159,6 +162,8 @@ class FakeOvsdbSbOvnIdl(object):
('fake', 'fake-dp')
self.get_chassis_and_physnets = mock.Mock()
self.get_gateway_chassis_from_cms_options = mock.Mock()
self.is_col_present = mock.Mock()
self.is_col_present.return_value = False


class FakeOvsdbTransaction(object):

+ 88
- 0
networking_ovn/tests/unit/ml2/test_mech_driver.py View File

@@ -2918,3 +2918,91 @@ class TestOVNVtepPortBinding(OVNMechanismDriverTestCase):
ovn_port_info.options["vtep-physical-switch"])
self.assertEqual(port[OVN_PROFILE]["vtep-logical-switch"],
ovn_port_info.options["vtep-logical-switch"])


@mock.patch('networking_ovn.common.ovn_client.OVNClient'
'._is_virtual_port_supported', lambda *args: True)
class TestOVNVVirtualPort(OVNMechanismDriverTestCase):

def setUp(self):
super(TestOVNVVirtualPort, self).setUp()
self.context = context.get_admin_context()
self.nb_idl = self.mech_driver._ovn_client._nb_idl
self.net = self._make_network(
self.fmt, name='net1', admin_state_up=True)['network']
self.subnet = self._make_subnet(
self.fmt, {'network': self.net},
'10.0.0.1', '10.0.0.0/24')['subnet']

@mock.patch('networking_ovn.common.ovn_client.OVNClient.'
'get_virtual_port_parents')
def test_create_port_with_virtual_type_and_options(self, mock_get_parents):
fake_parents = ['parent-0', 'parent-1']
mock_get_parents.return_value = fake_parents
port = {'id': 'virt-port',
'mac_address': '00:00:00:00:00:00',
'device_owner': '',
'network_id': self.net['id'],
'fixed_ips': [{'subnet_id': self.subnet['id'],
'ip_address': '10.0.0.55'}]}
port_info = self.mech_driver._ovn_client._get_port_options(
port)
self.assertEqual(ovn_const.LSP_TYPE_VIRTUAL, port_info.type)
self.assertEqual(
'10.0.0.55',
port_info.options[ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY])
self.assertIn(
'parent-0',
port_info.options[
ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY])
self.assertIn(
'parent-1',
port_info.options[
ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY])

@mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_ports')
def _test_set_unset_virtual_port_type(self, mock_get_ports, unset=False):
cmd = self.nb_idl.set_lswitch_port_to_virtual_type
if unset:
cmd = self.nb_idl.unset_lswitch_port_to_virtual_type

fake_txn = mock.Mock()
parent_port = {'id': 'parent-port', 'network_id': 'fake-network'}
port = {'id': 'virt-port'}
mock_get_ports.return_value = [port]
self.mech_driver._ovn_client._set_unset_virtual_port_type(
self.context, fake_txn, parent_port, ['10.0.0.55'], unset=unset)

args = {'lport_name': 'virt-port',
'virtual_parent': 'parent-port',
'if_exists': True}
if not unset:
args['vip'] = '10.0.0.55'

cmd.assert_called_once_with(**args)

def test__set_unset_virtual_port_type_set(self):
self._test_set_unset_virtual_port_type(unset=False)

def test__set_unset_virtual_port_type_unset(self):
self._test_set_unset_virtual_port_type(unset=True)

def test_delete_virtual_port_parent(self):
self.nb_idl.ls_get.return_value.execute.return_value = (
fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={'ports': []}))
virt_port = self._make_port(self.fmt, self.net['id'])['port']
virt_ip = virt_port['fixed_ips'][0]['ip_address']
parent = self._make_port(
self.fmt, self.net['id'],
allowed_address_pairs=[{'ip_address': virt_ip}])['port']
fake_row = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'name': virt_port['id'],
'type': ovn_const.LSP_TYPE_VIRTUAL,
'options': {ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY:
parent['id']}})
self.nb_idl.ls_get.return_value.execute.return_value = (
mock.Mock(ports=[fake_row]))

self.mech_driver._ovn_client.delete_port(parent['id'])
self.nb_idl.unset_lswitch_port_to_virtual_type.assert_called_once_with(
virt_port['id'], parent['id'], if_exists=True)

+ 9
- 0
releasenotes/notes/virtual-ports-fe725a817ce45e6d.yaml View File

@@ -0,0 +1,9 @@
---
fixes:
- |
Upon adding an IP address to the allowed_address_pairs field of a
Neutron's port, networking-ovn will look if that IP address matches
with the IP of an existing port in the same network and set its
type to "virtual" (if it does match). By doing that, networking-ovn
tells OVN that this virtual port is not bound to any VIF (required
for VRRP configuration).

Loading…
Cancel
Save