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:
parent
6c9de5324b
commit
ed946c4d55
@ -114,6 +114,11 @@ This method requires a Node UUID and the physical hardware address for the Port
|
||||
.. versionadded:: 1.88
|
||||
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
|
||||
|
||||
Request
|
||||
@ -323,6 +328,11 @@ Update a Port.
|
||||
.. versionadded:: 1.88
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
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
|
||||
========
|
||||
|
||||
|
@ -2,6 +2,31 @@
|
||||
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)
|
||||
-----------------------
|
||||
|
||||
|
@ -114,6 +114,20 @@ def hide_fields_in_newer_versions(port):
|
||||
# if requested version is < 1.88, hide name field.
|
||||
if not api_utils.allow_port_name():
|
||||
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):
|
||||
@ -350,6 +364,10 @@ class PortsController(rest.RestController):
|
||||
if (not api_utils.allow_local_link_connection_network_type()
|
||||
and 'network_type' in fields['local_link_connection']):
|
||||
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
|
||||
and not api_utils.allow_port_name()):
|
||||
raise exception.NotAcceptable()
|
||||
|
@ -103,8 +103,14 @@ LOCAL_LINK_BASE_SCHEMA = {
|
||||
'switch_info': {'type': 'string'},
|
||||
'network_type': {'type': 'string',
|
||||
'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)
|
||||
@ -115,6 +121,11 @@ LOCAL_LINK_SMART_NIC_SCHEMA = copy.deepcopy(LOCAL_LINK_BASE_SCHEMA)
|
||||
# set mandatory fields for a smart nic
|
||||
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
|
||||
LOCAL_LINK_UNMANAGED_SCHEMA = copy.deepcopy(LOCAL_LINK_BASE_SCHEMA)
|
||||
LOCAL_LINK_UNMANAGED_SCHEMA['properties']['network_type']['enum'] = [
|
||||
@ -125,6 +136,7 @@ LOCAL_LINK_CONN_SCHEMA = {'anyOf': [
|
||||
LOCAL_LINK_SCHEMA,
|
||||
LOCAL_LINK_SMART_NIC_SCHEMA,
|
||||
LOCAL_LINK_UNMANAGED_SCHEMA,
|
||||
LOCAL_LINK_OVN_SCHEMA,
|
||||
{'type': 'object', 'additionalProperties': False},
|
||||
]}
|
||||
|
||||
@ -163,7 +175,7 @@ def local_link_normalize(name, value):
|
||||
except exception.InvalidDatapathID:
|
||||
raise exception.InvalidSwitchID(switch_id=value['switch_id'])
|
||||
except KeyError:
|
||||
# In Smart NIC case 'switch_id' is optional.
|
||||
# In Smart NIC or OVN VTEP case 'switch_id' is optional.
|
||||
pass
|
||||
|
||||
return value
|
||||
@ -1887,6 +1899,15 @@ def check_volume_policy_and_retrieve(policy_name, vol_ident, target=False):
|
||||
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():
|
||||
"""Check if building configdrive is allowed.
|
||||
|
||||
|
@ -127,6 +127,7 @@ BASE_VERSION = 1
|
||||
# v1.87: Add service verb
|
||||
# v1.88: Add name field to port.
|
||||
# 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_1_INITIAL_VERSION = 1
|
||||
@ -218,6 +219,7 @@ MINOR_86_FIRMWARE_INTERFACE = 86
|
||||
MINOR_87_SERVICE = 87
|
||||
MINOR_88_PORT_NAME = 88
|
||||
MINOR_89_ATTACH_DETACH_VMEDIA = 89
|
||||
MINOR_90_OVN_VTEP = 90
|
||||
|
||||
# When adding another version, update:
|
||||
# - MINOR_MAX_VERSION
|
||||
@ -225,7 +227,7 @@ MINOR_89_ATTACH_DETACH_VMEDIA = 89
|
||||
# explanation of what changed in the new version
|
||||
# - 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
|
||||
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||
|
@ -311,8 +311,20 @@ def add_ports_to_network(task, network_uuid, security_groups=None):
|
||||
continue
|
||||
|
||||
update_port_attrs['mac_address'] = ironic_port.address
|
||||
|
||||
# Stores local link information for the port
|
||||
binding_profile = {'local_link_information':
|
||||
[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
|
||||
|
||||
if not ironic_port.pxe_enabled:
|
||||
@ -380,6 +392,28 @@ def add_ports_to_network(task, network_uuid, security_groups=None):
|
||||
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):
|
||||
"""Deletes the neutron ports created for booting the ramdisk.
|
||||
|
||||
|
@ -617,7 +617,7 @@ RELEASE_MAPPING = {
|
||||
}
|
||||
},
|
||||
'master': {
|
||||
'api': '1.89',
|
||||
'api': '1.90',
|
||||
'rpc': '1.59',
|
||||
'objects': {
|
||||
'Allocation': ['1.1'],
|
||||
|
@ -384,6 +384,34 @@ class TestListPorts(test_api_base.BaseApiTest):
|
||||
headers={api_base.Version.string: "1.53"})
|
||||
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):
|
||||
fields = 'uuid,extra'
|
||||
for i in range(3):
|
||||
|
@ -1854,3 +1854,31 @@ class TestLocalLinkValidation(base.TestCase):
|
||||
v = utils.LOCAL_LINK_VALIDATOR
|
||||
value = {'network_type': 'invalid'}
|
||||
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))
|
||||
|
@ -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.
|
@ -41,7 +41,7 @@ keystonemiddleware>=9.5.0 # Apache-2.0
|
||||
oslo.messaging>=14.1.0 # Apache-2.0
|
||||
tenacity>=6.3.1 # 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
|
||||
futurist>=1.2.0 # Apache-2.0
|
||||
tooz>=2.7.0 # Apache-2.0
|
||||
|
Loading…
Reference in New Issue
Block a user