Browse Source

Add support for virtual port type

This patch adds support for "virtual" port type following the work in
core OVN [0].

Currently there are two main usages for this type of port:

* Octavia: For creating the logical port for the virtual IP.
* VRRP [1]

Upon adding an IP address to the allowed_address_pairs field of the
Neutron's port, networking-ovn will look if that IP matches with the IP
of another existing port in the same network. If so, networking-ovn will
updating the matching port accordingly setting its type to "virtual"
and adding the required options in the OVN database.

The patch also accounts for other situations such as:

* Creating the VIP port after the parents (the ones with the IP in the
  allowed_address_pairs field) are created.

* When updating removing/adding allowed_address_pairs' the virtual
  ports are also updated.

* When deleting a parent port the virtual ports are also updated.

The code removes the type "virtual" from a virtual port whenever there's
no parents left (in case of deletion or editing allowed_address_pairs)
making it an ordinary port again.

The patch also keeps the logic introduced by
33fd553158 for version of OVN which does
not support the virtual port type (> 2.12) making it backward compatible.

[0]
054f4c85c4
[1]
https://docs.catalystcloud.io/tutorials/deploying-highly-available-instances-with-keepalived.html

Closes-Bug: #1840449
Related-Bug: #1789686
Signed-off-by: Lucas Alvares Gomes <lucasagomes@gmail.com>
(cherry picked from commit 5e72ea104c)

Conflicts:
    networking_ovn/common/ovn_client.py
    networking_ovn/tests/functional/test_mech_driver.py
    networking_ovn/tests/unit/fakes.py
    networking_ovn/tests/unit/ml2/test_mech_driver.py

Change-Id: I0b01b764413d178759a43028428c212014d3aa80
changes/02/704902/5
Lucas Alvares Gomes 11 months ago
committed by Brian Haley
parent
commit
32eb8c3f01
10 changed files with 583 additions and 25 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. +241
    -2
      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

@@ -172,3 +172,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
@@ -35,6 +36,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>
@@ -435,3 +439,13 @@ def get_port_id_from_gwc_row(row):
:returns: String containing router port_id.
"""
return constants.RE_PORT_FROM_GWC.search(row.name).group(2)


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

@@ -1168,3 +1168,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

@@ -738,6 +738,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

@@ -623,6 +623,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):


+ 241
- 2
networking_ovn/tests/functional/test_mech_driver.py View File

@@ -12,11 +12,14 @@
# 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
from oslo_config import cfg
from oslo_utils import uuidutils


class TestPortBinding(base.TestOVNFunctionalBase):
@@ -174,3 +177,239 @@ 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

@@ -145,6 +145,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):
@@ -163,6 +166,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

@@ -2606,3 +2606,91 @@ class TestOVNMechanismDriverMetadataPort(test_plugin.Ml2PluginV2TestCase):
self.assertEqual(exc.HTTPNoContent.code,
res.status_int)
self.assertEqual(1, self.nb_ovn.delete_lswitch_port.call_count)


@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