diff --git a/api-ref/source/baremetal-api-v1-ports.inc b/api-ref/source/baremetal-api-v1-ports.inc index f957ce6870..ede6624a89 100644 --- a/api-ref/source/baremetal-api-v1-ports.inc +++ b/api-ref/source/baremetal-api-v1-ports.inc @@ -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 diff --git a/doc/source/admin/ovn-networking.rst b/doc/source/admin/ovn-networking.rst index c937da53cc..68a1f8adcd 100644 --- a/doc/source/admin/ovn-networking.rst +++ b/doc/source/admin/ovn-networking.rst @@ -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 `_, +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 ======== diff --git a/doc/source/contributor/webapi-version-history.rst b/doc/source/contributor/webapi-version-history.rst index e9d0049613..710e6b4dca 100644 --- a/doc/source/contributor/webapi-version-history.rst +++ b/doc/source/contributor/webapi-version-history.rst @@ -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) ----------------------- diff --git a/ironic/api/controllers/v1/port.py b/ironic/api/controllers/v1/port.py index 1299cc6820..13131152bd 100644 --- a/ironic/api/controllers/v1/port.py +++ b/ironic/api/controllers/v1/port.py @@ -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() diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py index b31988ee8b..cec0a789fd 100644 --- a/ironic/api/controllers/v1/utils.py +++ b/ironic/api/controllers/v1/utils.py @@ -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. diff --git a/ironic/api/controllers/v1/versions.py b/ironic/api/controllers/v1/versions.py index 3ba160154e..5bc9fc6448 100644 --- a/ironic/api/controllers/v1/versions.py +++ b/ironic/api/controllers/v1/versions.py @@ -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) diff --git a/ironic/common/neutron.py b/ironic/common/neutron.py index 7a5d92dd85..9b08341b0a 100644 --- a/ironic/common/neutron.py +++ b/ironic/common/neutron.py @@ -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. diff --git a/ironic/common/release_mappings.py b/ironic/common/release_mappings.py index 6946881416..7b00842b8c 100644 --- a/ironic/common/release_mappings.py +++ b/ironic/common/release_mappings.py @@ -617,7 +617,7 @@ RELEASE_MAPPING = { } }, 'master': { - 'api': '1.89', + 'api': '1.90', 'rpc': '1.59', 'objects': { 'Allocation': ['1.1'], diff --git a/ironic/tests/unit/api/controllers/v1/test_port.py b/ironic/tests/unit/api/controllers/v1/test_port.py index d971e50c75..eb5cbb7df1 100644 --- a/ironic/tests/unit/api/controllers/v1/test_port.py +++ b/ironic/tests/unit/api/controllers/v1/test_port.py @@ -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): diff --git a/ironic/tests/unit/api/controllers/v1/test_utils.py b/ironic/tests/unit/api/controllers/v1/test_utils.py index d12e4328a5..1f12798749 100644 --- a/ironic/tests/unit/api/controllers/v1/test_utils.py +++ b/ironic/tests/unit/api/controllers/v1/test_utils.py @@ -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)) diff --git a/releasenotes/notes/ovn-vtep-switch-support-506686368ebf17c6.yaml b/releasenotes/notes/ovn-vtep-switch-support-506686368ebf17c6.yaml new file mode 100644 index 0000000000..87f0ca6307 --- /dev/null +++ b/releasenotes/notes/ovn-vtep-switch-support-506686368ebf17c6.yaml @@ -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. diff --git a/requirements.txt b/requirements.txt index 7df883dc07..e101ed0acf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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