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
|
.. 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
|
||||||
|
|
||||||
|
@ -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
|
||||||
========
|
========
|
||||||
|
|
||||||
|
@ -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)
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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'],
|
||||||
|
@ -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):
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
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
|
||||||
|
Loading…
Reference in New Issue
Block a user