Basic support for OVN VTEP switches

Adds basic support for passing OVN VTEP switch metadata to
neutron via Ironic's port.local_link_connection field.

Adds microversion 1.90 to Ironic's API, adding support for
new schema in port.local_link_connection

Bump version of the jsonschema library to ensure consistent
behavior with new schema configurations.

Add documentation warning: This has not been tested as no
Ironic developers have access to the hardware in question.

Closes-bug: #2034953
Co-Authored-By: Austin Cormier <acormier@juniper.net>
Co-Authored-By: Jay Faulkner <jay@jvf.cc>
Change-Id: Ie98dc4552ec2ea16db1e2d382aed54ce9dfef41b
This commit is contained in:
Boushra Bettir 2023-11-09 16:37:58 -08:00 committed by Jay Faulkner
parent 6c9de5324b
commit ed946c4d55
12 changed files with 190 additions and 5 deletions

View File

@ -114,6 +114,11 @@ This method requires a Node UUID and the physical hardware address for the Port
.. versionadded:: 1.88 .. versionadded:: 1.88
Added the ``name`` field. Added the ``name`` field.
.. versionadded:: 1.90
``local_link_connection`` fields now accepts a dictionary
of ``vtep-logical-switch``, ``vtep-physical-switch`` and ``port_id``
to identify ovn vtep switches.
Normal response code: 201 Normal response code: 201
Request Request
@ -323,6 +328,11 @@ Update a Port.
.. versionadded:: 1.88 .. versionadded:: 1.88
Added the ``name`` Added the ``name``
.. versionadded:: 1.90
``local_link_connection`` fields now accepts a dictionary
of ``vtep-logical-switch``, ``vtep-physical-switch`` and ``port_id``
to identify ovn vtep switches.
Normal response code: 200 Normal response code: 200

View File

@ -150,6 +150,19 @@ above and beyond a dedicated interface, you will need to make the attachment
on the ``br-ex`` integration bridge, as opposed to ``br-int`` as one would on the ``br-ex`` integration bridge, as opposed to ``br-int`` as one would
have done with OVS. have done with OVS.
VTEP Switch Support
===================
Alpha-quality support was added to Ironic for OVN VTEP switches in API version
1.90. When the keys ``vtep-logical-switch``, ``vtep-physical-switch``, and
``port_id`` are set in ``port.local_link_connection``, Ironic will pass them on
to Neutron to be included in the binding profile to enable OVN support.
There `are reports of this approach working <https://bugs.launchpad.net/ironic/+bug/2034953>`_,
but Ironic developers do not have access to physical hardware to fully test
this feature. If you have any feedback for this feature, please reach out
to the Ironic community.
Unknowns Unknowns
======== ========

View File

@ -2,6 +2,31 @@
REST API Version History REST API Version History
======================== ========================
1.90 (Caracal)
-----------------------
API supports ovn vtep switches as a valid schema for
``port.local_link_connection``. Ovn vtep switches are represented
as the following:
.. code-block:: json
{
"port_id": "exampleportid",
"vtep-logical-switch": "examplelogicalswitch",
"vtep-physical-switch": "examplephysicalswitch"
}
1.89 (Caracal)
---------------------------------
Adds support to attaching or detaching images from a node's virtual
media using the ``/v1/nodes/{node_ident}/vmedia`` endpoint. A ``POST``
request containing ``device_type``, ``image_url``,
and ``image_download_source`` will attach the requested image to the
node's virtual media. A later ``DELETE`` request to the same endpoint
will detach it.
1.88 (Bobcat) 1.88 (Bobcat)
----------------------- -----------------------

View File

@ -114,6 +114,20 @@ def hide_fields_in_newer_versions(port):
# if requested version is < 1.88, hide name field. # if requested version is < 1.88, hide name field.
if not api_utils.allow_port_name(): if not api_utils.allow_port_name():
port.pop('name', None) port.pop('name', None)
# note(JayF): if requested version is < 1.90, hide new
# local_link_connection schema but only check it if we allow advanced
# net fields, since otherwise we removed local_link_connection above
# and don't want to re-add it here
if (not api_utils.allow_ovn_vtep_version()
and api_utils.allow_port_advanced_net_fields):
local_link_connection = port.get('local_link_connection', {})
if any(key for key in local_link_connection.keys()
if key in api_utils.LOCAL_LINK_OVN_90_FIELDS):
# note(JayF): In this case, the field *should* exist but should be
# set to empty. This is because api version clients in this branch
# expect the key port.local_link_connection to exist even if we
# cannot set a valid value
port['local_link_connection'] = {}
def convert_with_links(rpc_port, fields=None, sanitize=True): def convert_with_links(rpc_port, fields=None, sanitize=True):
@ -350,6 +364,10 @@ class PortsController(rest.RestController):
if (not api_utils.allow_local_link_connection_network_type() if (not api_utils.allow_local_link_connection_network_type()
and 'network_type' in fields['local_link_connection']): and 'network_type' in fields['local_link_connection']):
raise exception.NotAcceptable() raise exception.NotAcceptable()
if (not api_utils.allow_ovn_vtep_version()
and 'vtep-logical-switch'
in fields['local_link_connection']):
raise exception.NotAcceptable()
if ('name' in fields if ('name' in fields
and not api_utils.allow_port_name()): and not api_utils.allow_port_name()):
raise exception.NotAcceptable() raise exception.NotAcceptable()

View File

@ -103,8 +103,14 @@ LOCAL_LINK_BASE_SCHEMA = {
'switch_info': {'type': 'string'}, 'switch_info': {'type': 'string'},
'network_type': {'type': 'string', 'network_type': {'type': 'string',
'enum': ['managed', 'unmanaged']}, 'enum': ['managed', 'unmanaged']},
'vtep-logical-switch': {'type': 'string'},
'vtep-physical-switch': {'type': 'string'},
}, },
'additionalProperties': False 'additionalProperties': False,
'dependentRequired': {
'vtep-logical-switch': ['vtep-physical-switch'],
'vtep-physical-switch': ['vtep-logical-switch']
}
} }
LOCAL_LINK_SCHEMA = copy.deepcopy(LOCAL_LINK_BASE_SCHEMA) LOCAL_LINK_SCHEMA = copy.deepcopy(LOCAL_LINK_BASE_SCHEMA)
@ -115,6 +121,11 @@ LOCAL_LINK_SMART_NIC_SCHEMA = copy.deepcopy(LOCAL_LINK_BASE_SCHEMA)
# set mandatory fields for a smart nic # set mandatory fields for a smart nic
LOCAL_LINK_SMART_NIC_SCHEMA['required'] = ['port_id', 'hostname'] LOCAL_LINK_SMART_NIC_SCHEMA['required'] = ['port_id', 'hostname']
LOCAL_LINK_OVN_SCHEMA = copy.deepcopy(LOCAL_LINK_BASE_SCHEMA)
LOCAL_LINK_OVN_SCHEMA['required'] = ['port_id', 'vtep-logical-switch',
'vtep-physical-switch']
LOCAL_LINK_OVN_90_FIELDS = ['vtep-logical-switch', 'vtep-physical-switch']
# no other mandatory fields for a network_type=unmanaged link # no other mandatory fields for a network_type=unmanaged link
LOCAL_LINK_UNMANAGED_SCHEMA = copy.deepcopy(LOCAL_LINK_BASE_SCHEMA) LOCAL_LINK_UNMANAGED_SCHEMA = copy.deepcopy(LOCAL_LINK_BASE_SCHEMA)
LOCAL_LINK_UNMANAGED_SCHEMA['properties']['network_type']['enum'] = [ LOCAL_LINK_UNMANAGED_SCHEMA['properties']['network_type']['enum'] = [
@ -125,6 +136,7 @@ LOCAL_LINK_CONN_SCHEMA = {'anyOf': [
LOCAL_LINK_SCHEMA, LOCAL_LINK_SCHEMA,
LOCAL_LINK_SMART_NIC_SCHEMA, LOCAL_LINK_SMART_NIC_SCHEMA,
LOCAL_LINK_UNMANAGED_SCHEMA, LOCAL_LINK_UNMANAGED_SCHEMA,
LOCAL_LINK_OVN_SCHEMA,
{'type': 'object', 'additionalProperties': False}, {'type': 'object', 'additionalProperties': False},
]} ]}
@ -163,7 +175,7 @@ def local_link_normalize(name, value):
except exception.InvalidDatapathID: except exception.InvalidDatapathID:
raise exception.InvalidSwitchID(switch_id=value['switch_id']) raise exception.InvalidSwitchID(switch_id=value['switch_id'])
except KeyError: except KeyError:
# In Smart NIC case 'switch_id' is optional. # In Smart NIC or OVN VTEP case 'switch_id' is optional.
pass pass
return value return value
@ -1887,6 +1899,15 @@ def check_volume_policy_and_retrieve(policy_name, vol_ident, target=False):
return rpc_vol, rpc_node return rpc_vol, rpc_node
def allow_ovn_vtep_version():
"""Check if ovn vtep version is allowed.
Version 1.90 of the API added support for ovn
vtep switches in port.local_link_connection.
"""
return api.request.version.minor >= versions.MINOR_90_OVN_VTEP
def allow_build_configdrive(): def allow_build_configdrive():
"""Check if building configdrive is allowed. """Check if building configdrive is allowed.

View File

@ -127,6 +127,7 @@ BASE_VERSION = 1
# v1.87: Add service verb # v1.87: Add service verb
# v1.88: Add name field to port. # v1.88: Add name field to port.
# v1.89: Add API for attaching/detaching virtual media # v1.89: Add API for attaching/detaching virtual media
# v1.90: Accept ovn vtep switch metadata schema to port.local_link_connection
MINOR_0_JUNO = 0 MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1 MINOR_1_INITIAL_VERSION = 1
@ -218,6 +219,7 @@ MINOR_86_FIRMWARE_INTERFACE = 86
MINOR_87_SERVICE = 87 MINOR_87_SERVICE = 87
MINOR_88_PORT_NAME = 88 MINOR_88_PORT_NAME = 88
MINOR_89_ATTACH_DETACH_VMEDIA = 89 MINOR_89_ATTACH_DETACH_VMEDIA = 89
MINOR_90_OVN_VTEP = 90
# When adding another version, update: # When adding another version, update:
# - MINOR_MAX_VERSION # - MINOR_MAX_VERSION
@ -225,7 +227,7 @@ MINOR_89_ATTACH_DETACH_VMEDIA = 89
# explanation of what changed in the new version # explanation of what changed in the new version
# - common/release_mappings.py, RELEASE_MAPPING['master']['api'] # - common/release_mappings.py, RELEASE_MAPPING['master']['api']
MINOR_MAX_VERSION = MINOR_89_ATTACH_DETACH_VMEDIA MINOR_MAX_VERSION = MINOR_90_OVN_VTEP
# String representations of the minor and maximum versions # String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION) _MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -311,8 +311,20 @@ def add_ports_to_network(task, network_uuid, security_groups=None):
continue continue
update_port_attrs['mac_address'] = ironic_port.address update_port_attrs['mac_address'] = ironic_port.address
# Stores local link information for the port
binding_profile = {'local_link_information': binding_profile = {'local_link_information':
[portmap[ironic_port.uuid]]} [portmap[ironic_port.uuid]]}
# Determine if network type is OVN
if is_ovn_vtep_port(ironic_port):
vtep_logical_switch = \
portmap[ironic_port.uuid]['vtep_logical_switch']
vtep_physical_switch = \
portmap[ironic_port.uuid]['vtep_physical_switch']
binding_profile['vtep_logical_switch'] = vtep_logical_switch
binding_profile['vtep_physical_switch'] = vtep_physical_switch
update_port_attrs['binding:profile'] = binding_profile update_port_attrs['binding:profile'] = binding_profile
if not ironic_port.pxe_enabled: if not ironic_port.pxe_enabled:
@ -380,6 +392,28 @@ def add_ports_to_network(task, network_uuid, security_groups=None):
return ports return ports
def is_ovn_vtep_port(port_info):
"""Check if the current port is an OVN VTEP port
:param port_info: an instance of ironic.objects.port.Port
or port data as a port like object
:returns: Boolean indicating if the port is an OVN VTEP port
"""
local_link_connection = {}
if isinstance(port_info, objects.Port):
local_link_connection = port_info.local_link_connection
elif isinstance(port_info, dict):
local_link_connection = port_info['local_link_connection']
if all(k in local_link_connection.keys()
for k in ['vtep-logical-switch', 'vtep-physical-switch']):
return True
return False
def remove_ports_from_network(task, network_uuid): def remove_ports_from_network(task, network_uuid):
"""Deletes the neutron ports created for booting the ramdisk. """Deletes the neutron ports created for booting the ramdisk.

View File

@ -617,7 +617,7 @@ RELEASE_MAPPING = {
} }
}, },
'master': { 'master': {
'api': '1.89', 'api': '1.90',
'rpc': '1.59', 'rpc': '1.59',
'objects': { 'objects': {
'Allocation': ['1.1'], 'Allocation': ['1.1'],

View File

@ -384,6 +384,34 @@ class TestListPorts(test_api_base.BaseApiTest):
headers={api_base.Version.string: "1.53"}) headers={api_base.Version.string: "1.53"})
self.assertTrue(data['is_smartnic']) self.assertTrue(data['is_smartnic'])
def test_hide_fields_in_newer_versions_ovn_vtep(self):
llc = {'port_id': '42',
'vtep-logical-switch': 'lswitch',
'vtep-physical-switch': 'jswitch'}
port = obj_utils.create_test_port(self.context, node_id=self.node.id,
local_link_connection=llc)
# note(JayF): Version older than 1.19, older than 1.90,
# this means port.llc key does not exist at all.
data = self.get_json(
'/ports/%s' % port.uuid,
headers={api_base.Version.string: "1.18"})
self.assertNotIn('local_link_connection', data)
# note(JayF): Version newer than 1.19, older than 1.90,
# this means port.llc key must exist, value is empty dict
data = self.get_json(
'/ports/%s' % port.uuid,
headers={api_base.Version.string: "1.89"})
self.assertIn('local_link_connection', data)
self.assertEqual({}, data['local_link_connection'])
# note(JayF): Version 1.90+, key exists, value is passed
data = self.get_json('/ports/%s' % port.uuid,
headers={api_base.Version.string: "1.90"})
self.assertIn('local_link_connection', data)
self.assertEqual(llc, data['local_link_connection'])
def test_get_collection_custom_fields(self): def test_get_collection_custom_fields(self):
fields = 'uuid,extra' fields = 'uuid,extra'
for i in range(3): for i in range(3):

View File

@ -1854,3 +1854,31 @@ class TestLocalLinkValidation(base.TestCase):
v = utils.LOCAL_LINK_VALIDATOR v = utils.LOCAL_LINK_VALIDATOR
value = {'network_type': 'invalid'} value = {'network_type': 'invalid'}
self.assertRaises(exception.Invalid, v, 'l', value) self.assertRaises(exception.Invalid, v, 'l', value)
def test_local_link_connection_cant_set_only_physical(self):
v = utils.LOCAL_LINK_VALIDATOR
value = {'port_id': '42',
'vtep-physical-switch': 'jswitch',
'switch_id': '0a:1b:2c:3d:4e:5f'}
self.assertRaisesRegex(
exception.Invalid,
'is a dependency of',
v, 'l', value)
def test_local_link_connection_cant_set_only_logical(self):
v = utils.LOCAL_LINK_VALIDATOR
value = {'port_id': '42',
'vtep-logical-switch': 'jswitch',
'switch_id': '0a:1b:2c:3d:4e:5f'}
self.assertRaisesRegex(
exception.Invalid,
'is a dependency of',
v, 'l', value
)
def test_local_link_connection_set_both_switches(self):
v = utils.LOCAL_LINK_VALIDATOR
value = {'port_id': '42',
'vtep-logical-switch': 'lswitch',
'vtep-physical-switch': 'pswitch'}
self.assertEqual(value, v('l', value))

View File

@ -0,0 +1,6 @@
---
features:
- |
Add support for ovn vtep switches. Operators will be able
to use logical and physical switches. Minimally tested
in production.

View File

@ -41,7 +41,7 @@ keystonemiddleware>=9.5.0 # Apache-2.0
oslo.messaging>=14.1.0 # Apache-2.0 oslo.messaging>=14.1.0 # Apache-2.0
tenacity>=6.3.1 # Apache-2.0 tenacity>=6.3.1 # Apache-2.0
oslo.versionedobjects>=1.31.2 # Apache-2.0 oslo.versionedobjects>=1.31.2 # Apache-2.0
jsonschema>=3.2.0 # MIT jsonschema>=4.19.0 # MIT
psutil>=3.2.2 # BSD psutil>=3.2.2 # BSD
futurist>=1.2.0 # Apache-2.0 futurist>=1.2.0 # Apache-2.0
tooz>=2.7.0 # Apache-2.0 tooz>=2.7.0 # Apache-2.0