Expose is_smartnic in port API

Changes are made to support ironic handling is_smarting port attribute
that was added in this change Ic2ffbd6f1035907ea5a18bda6d2b21e617194195

This change expose is_smartnic port field in REST API, updated API
reference to include is_smartnic field and relevent documentations.

API version updated to 1.53.

Story: #2003346
Change-Id: I89ce54a0c034f2a5f82ff961ab06cfcc6d853bd4
This commit is contained in:
Hamdy Khader 2019-01-13 15:40:30 +02:00
parent 506cb12160
commit 6325b6c13c
26 changed files with 409 additions and 36 deletions

View File

@ -11,7 +11,7 @@ fi
OS_AUTH_TOKEN=$(openstack token issue | grep ' id ' | awk '{print $4}')
IRONIC_URL="http://127.0.0.1:6385"
IRONIC_API_VERSION="1.37"
IRONIC_API_VERSION="1.53"
export OS_AUTH_TOKEN IRONIC_URL

View File

@ -32,6 +32,9 @@ Return a list of bare metal Ports associated with ``node_ident``.
.. versionadded:: 1.34
Added the ``physical_network`` field.
.. versionadded:: 1.53
Added the ``is_smartnic`` response fields.
Normal response code: 200
Error codes: TBD
@ -79,6 +82,9 @@ Return a detailed list of bare metal Ports associated with ``node_ident``.
.. versionadded:: 1.34
Added the ``physical_network`` field.
.. versionadded:: 1.53
Added the ``is_smartnic`` response fields.
Normal response code: 200
Error codes: TBD
@ -112,6 +118,7 @@ Response
- created_at: created_at
- updated_at: updated_at
- links: links
- is_smartnic: is_smartnic
**Example details of a Node's Ports:**

View File

@ -25,6 +25,9 @@ Response to include only the specified fields, rather than the default set.
.. versionadded:: 1.34
Added the ``physical_network`` field.
.. versionadded:: 1.53
Added the ``is_smartnic`` response fields.
Normal response code: 200
Error codes: 400,401,403,404
@ -66,6 +69,9 @@ Return a detailed list of bare metal Ports associated with ``portgroup_ident``.
.. versionadded:: 1.34
Added the ``physical_network`` field.
.. versionadded:: 1.53
Added the ``is_smartnic`` response fields.
Normal response code: 200
Error codes: 400,401,403,404
@ -99,6 +105,7 @@ Response
- created_at: created_at
- updated_at: updated_at
- links: links
- is_smartnic: is_smartnic
**Example details of a Portgroup's Ports:**

View File

@ -46,6 +46,9 @@ By default, this query will return the uuid and address for each Port.
Added the ``detail`` boolean request parameter. When specified ``True`` this
causes the response to include complete details about each port.
.. versionadded:: 1.53
Added the ``is_smartnic`` field.
Normal response code: 200
Request
@ -100,6 +103,9 @@ This method requires a Node UUID and the physical hardware address for the Port
.. versionadded:: 1.34
Added the ``physical_network`` request and response fields.
.. versionadded:: 1.53
Added the ``is_smartnic`` request and response fields.
Normal response code: 201
Request
@ -114,6 +120,7 @@ Request
- pxe_enabled: req_pxe_enabled
- physical_network: req_physical_network
- extra: req_extra
- is_smartnic: req_is_smartnic
**Example Port creation request:**
@ -137,6 +144,7 @@ Response
- created_at: created_at
- updated_at: updated_at
- links: links
- is_smartnic: is_smartnic
**Example Port creation response:**
@ -165,6 +173,9 @@ Return a list of bare metal Ports, with detailed information.
.. versionadded:: 1.34
Added the ``physical_network`` response field.
.. versionadded:: 1.53
Added the ``is_smartnic`` response fields.
Normal response code: 200
Request
@ -199,6 +210,7 @@ Response
- created_at: created_at
- updated_at: updated_at
- links: links
- is_smartnic: is_smartnic
**Example detailed Port list response:**
@ -227,6 +239,9 @@ Show details for the given Port.
.. versionadded:: 1.34
Added the ``physical_network`` response field.
.. versionadded:: 1.53
Added the ``is_smartnic`` response fields.
Normal response code: 200
Request
@ -254,6 +269,7 @@ Response
- created_at: created_at
- updated_at: updated_at
- links: links
- is_smartnic: is_smartnic
**Example Port details:**
@ -277,6 +293,9 @@ Update a Port.
.. versionadded:: 1.34
Added the ``physical_network`` field.
.. versionadded:: 1.53
Added the ``is_smartnic`` fields.
Normal response code: 200
Request
@ -311,6 +330,7 @@ Response
- created_at: created_at
- updated_at: updated_at
- links: links
- is_smartnic: is_smartnic
**Example Port update response:**

View File

@ -727,6 +727,12 @@ internal_info:
in: body
required: true
type: JSON
is_smartnic:
description: |
Indicates whether the Port is a Smart NIC port.
in: body
required: false
type: boolean
last_error:
description: |
Any error from the most recent (last) transaction that started but failed to finish.
@ -1133,6 +1139,12 @@ req_inspect_interface:
in: body
required: false
type: string
req_is_smartnic:
description: |
Indicates whether the Port is a Smart NIC port.
in: body
required: false
type: boolean
req_local_link_connection:
description: |
The Port binding profile. If specified, must contain ``switch_id`` (only

View File

@ -5,6 +5,7 @@
"created_at": "2016-08-18T22:28:48.643434+11:11",
"extra": {},
"internal_info": {},
"is_smartnic": true,
"links": [
{
"href": "http://127.0.0.1:6385/v1/ports/d2b30520-907d-46c8-bfee-c5586e6fb3a1",

View File

@ -2,6 +2,7 @@
"node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d",
"portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a",
"address": "11:11:11:11:11:11",
"is_smartnic": true,
"local_link_connection": {
"switch_id": "0a:1b:2c:3d:4e:5f",
"port_id": "Ethernet3/1",

View File

@ -3,6 +3,7 @@
"created_at": "2016-08-18T22:28:48.643434+11:11",
"extra": {},
"internal_info": {},
"is_smartnic": true,
"links": [
{
"href": "http://127.0.0.1:6385/v1/ports/d2b30520-907d-46c8-bfee-c5586e6fb3a1",

View File

@ -5,6 +5,7 @@
"created_at": "2016-08-18T22:28:48.643434+11:11",
"extra": {},
"internal_info": {},
"is_smartnic": true,
"links": [
{
"href": "http://127.0.0.1:6385/v1/ports/d2b30520-907d-46c8-bfee-c5586e6fb3a1",

View File

@ -3,6 +3,7 @@
"created_at": "2016-08-18T22:28:48.643434+11:11",
"extra": {},
"internal_info": {},
"is_smartnic": true,
"links": [
{
"href": "http://127.0.0.1:6385/v1/ports/d2b30520-907d-46c8-bfee-c5586e6fb3a1",

View File

@ -5,6 +5,7 @@
"created_at": "2016-08-18T22:28:48.643434+11:11",
"extra": {},
"internal_info": {},
"is_smartnic": true,
"links": [
{
"href": "http://127.0.0.1:6385/v1/ports/d2b30520-907d-46c8-bfee-c5586e6fb3a1",

View File

@ -58,16 +58,22 @@ network.
- Required. Identifies a switch and can be a MAC address or an
OpenFlow-based ``datapath_id``.
* - ``port_id``
- Required. Port ID on the switch, for example, Gig0/1.
- Required. Port ID on the switch/Smart NIC, for example, Gig0/1, rep0-0.
* - ``switch_info``
- Optional. Used to distinguish different switch models or other
vendor-specific identifier. Some ML2 plugins may require this
field.
* - ``hostname``
- Required in case of a Smart NIC port.
Hostname of Smart NIC device.
.. note::
This isn't applicable to Infiniband ports because the network topology
is discoverable by the Infiniband Subnet Manager.
If specified, local_link_connection information will be ignored.
If port is Smart NIC port then:
1. ``port_id`` is the representor port name on the Smart NIC.
2. ``switch_id`` is not mandatory.
.. _multitenancy-physnets:
@ -113,8 +119,11 @@ Configuring nodes
* Physical network support for ironic ports was added in API version 1.34,
and is supported by python-ironicclient version 1.15.0 or higher.
* Smart NIC support for ironic ports was added in API version 1.53,
and is supported by python-ironicclient version 2.7.0 or higher.
The following examples assume you are using python-ironicclient version
1.15.0 or higher.
2.7.0 or higher.
Export the following variable::
@ -165,6 +174,17 @@ Configuring nodes
--extra client-id=$CLIENT_ID \
--physical-network physnet1
#. Create a Smart NIC port as follows::
openstack baremetal port create $HW_MAC_ADDRESS --node $NODE_UUID \
--local-link-connection hostname=$HOSTNAME \
--local-link-connection port_id=$REP_NAME \
--pxe-enabled true \
--physical-network physnet1 \
--is-smartnic true
A Smart NIC port requires ``hostname`` which is the hostname of the Smart NIC,
and ``port_id`` which is the representor port name within the Smart NIC.
#. Check the port configuration::

View File

@ -211,12 +211,13 @@ Example of port CRUD notification::
"payload":{
"ironic_object.namespace":"ironic",
"ironic_object.name":"PortCRUDPayload",
"ironic_object.version":"1.2",
"ironic_object.version":"1.3",
"ironic_object.data":{
"address": "77:66:23:34:11:b7",
"created_at": "2016-02-11T15:23:03+00:00",
"node_uuid": "5b236cab-ad4e-4220-b57c-e827e858745a",
"extra": {},
"is_smartnic": True,
"local_link_connection": {},
"physical_network": "physnet1",
"portgroup_uuid": "bd2f385e-c51c-4752-82d1-7a9ec2c25f24",

View File

@ -2,6 +2,13 @@
REST API Version History
========================
1.53 (Stein, master)
--------------------
Added ``is_smartnic`` field to the port object to enable Smart NIC port
creation in addition to local link connection attributes ``port_id`` and
``hostname``.
1.52 (Stein, master)
--------------------

View File

@ -59,6 +59,9 @@ def hide_fields_in_newer_versions(obj):
# if requested version is < 1.34, hide physical_network field.
if not api_utils.allow_port_physical_network():
obj.physical_network = wsme.Unset
# if requested version is < 1.53, hide is_smartnic field.
if not api_utils.allow_port_is_smartnic():
obj.is_smartnic = wsme.Unset
class Port(base.APIBase):
@ -156,6 +159,9 @@ class Port(base.APIBase):
links = wsme.wsattr([link.Link], readonly=True)
"""A list containing a self link and associated port links"""
is_smartnic = types.boolean
"""Indicates whether this port is a Smart NIC port."""
def __init__(self, **kwargs):
self.fields = []
fields = list(objects.Port.fields)
@ -245,7 +251,8 @@ class Port(base.APIBase):
local_link_connection={
'switch_info': 'host', 'port_id': 'Gig0/1',
'switch_id': 'aa:bb:cc:dd:ee:ff'},
physical_network='physnet1')
physical_network='physnet1',
is_smartnic=False)
# NOTE(lucasagomes): node_uuid getter() method look at the
# _node_uuid variable
sample._node_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
@ -425,6 +432,9 @@ class PortsController(rest.RestController):
if ('physical_network' in fields
and not api_utils.allow_port_physical_network()):
raise exception.NotAcceptable()
if ('is_smartnic' in fields
and not api_utils.allow_port_is_smartnic()):
raise exception.NotAcceptable()
@METRICS.timer('PortsController.get_all')
@expose.expose(PortCollection, types.uuid_or_name, types.uuid,
@ -577,6 +587,12 @@ class PortsController(rest.RestController):
pdict = port.as_dict()
self._check_allowed_port_fields(pdict)
if (port.is_smartnic and not types.locallinkconnectiontype
.validate_for_smart_nic(port.local_link_connection)):
raise exception.Invalid(
"Smart NIC port must have port_id "
"and hostname in local_link_connection")
create_remotely = pecan.request.rpcapi.can_send_create_port()
if (not create_remotely and pdict.get('portgroup_uuid')):
# NOTE(mgoddard): In RPC API v1.41, port creation was moved to the
@ -652,7 +668,8 @@ class PortsController(rest.RestController):
fields_to_check = set()
for field in (self.advanced_net_fields
+ ['portgroup_uuid', 'physical_network']):
+ ['portgroup_uuid', 'physical_network',
'is_smartnic']):
field_path = '/%s' % field
if (api_utils.get_patch_values(patch, field_path)
or api_utils.is_path_removed(patch, field_path)):

View File

@ -18,6 +18,7 @@
import inspect
import json
from oslo_log import log
from oslo_utils import strutils
from oslo_utils import uuidutils
import six
@ -30,6 +31,9 @@ from ironic.common.i18n import _
from ironic.common import utils
LOG = log.getLogger(__name__)
class MacAddressType(wtypes.UserType):
"""A simple MAC address type."""
@ -266,9 +270,12 @@ class LocalLinkConnectionType(wtypes.UserType):
basetype = wtypes.DictType
name = 'locallinkconnection'
mandatory_fields = {'switch_id',
'port_id'}
valid_fields = mandatory_fields.union({'switch_info'})
local_link_mandatory_fields = {'port_id', 'switch_id'}
smart_nic_mandatory_fields = {'port_id', 'hostname'}
mandatory_fields_list = [local_link_mandatory_fields,
smart_nic_mandatory_fields]
optional_field = {'switch_info'}
valid_fields = set.union(optional_field, *mandatory_fields_list)
@staticmethod
def validate(value):
@ -276,7 +283,7 @@ class LocalLinkConnectionType(wtypes.UserType):
:param value: A dictionary of values to validate, switch_id is a MAC
address or an OpenFlow based datapath_id, switch_info is an
optional field.
optional field. Required Smart NIC fields are port_id and hostname.
For example::
@ -286,6 +293,13 @@ class LocalLinkConnectionType(wtypes.UserType):
'switch_info': 'switch1'
}
Or for Smart NIC::
{
'port_id': 'rep0-0',
'hostname': 'host1-bf'
}
:returns: A dictionary.
:raises: Invalid if some of the keys in the dictionary being validated
are unknown, invalid, or some required ones are missing.
@ -304,10 +318,20 @@ class LocalLinkConnectionType(wtypes.UserType):
if invalid:
raise exception.Invalid(_('%s are invalid keys') % (invalid))
# Check all mandatory fields are present
missing = LocalLinkConnectionType.mandatory_fields - keys
if missing:
msg = _('Missing mandatory keys: %s') % missing
# Check any mandatory fields sets are present
for mandatory_set in LocalLinkConnectionType.mandatory_fields_list:
if mandatory_set <= keys:
break
else:
msg = _('Missing mandatory keys. Required keys are '
'%(required_fields)s. Or in case of Smart NIC '
'%(smart_nic_required_fields)s. '
'Submitted keys are %(keys)s .') % {
'required_fields':
LocalLinkConnectionType.local_link_mandatory_fields,
'smart_nic_required_fields':
LocalLinkConnectionType.smart_nic_mandatory_fields,
'keys': keys}
raise exception.Invalid(msg)
# Check switch_id is either a valid mac address or
@ -321,6 +345,9 @@ class LocalLinkConnectionType(wtypes.UserType):
value['switch_id'])
except exception.InvalidDatapathID:
raise exception.InvalidSwitchID(switch_id=value['switch_id'])
except KeyError:
# In Smart NIC case 'switch_id' is optional.
pass
return value
@ -330,6 +357,21 @@ class LocalLinkConnectionType(wtypes.UserType):
return None
return LocalLinkConnectionType.validate(value)
@staticmethod
def validate_for_smart_nic(value):
"""Validates Smart NIC field are present 'port_id' and 'hostname'
:param value: local link information of type Dictionary.
:return: True if both fields 'port_id' and 'hostname' are present
in 'value', False otherwise.
"""
wtypes.DictType(wtypes.text, wtypes.text).validate(value)
keys = set(value)
if LocalLinkConnectionType.smart_nic_mandatory_fields <= keys:
return True
return False
locallinkconnectiontype = LocalLinkConnectionType()

View File

@ -1012,3 +1012,13 @@ def allow_allocations():
field for the node.
"""
return pecan.request.version.minor >= versions.MINOR_52_ALLOCATION
def allow_port_is_smartnic():
"""Check if port is_smartnic field is allowed.
Version 1.53 of the API added is_smartnic field to the port object.
"""
return ((pecan.request.version.minor
>= versions.MINOR_53_PORT_SMARTNIC)
and objects.Port.supports_is_smartnic())

View File

@ -90,6 +90,7 @@ BASE_VERSION = 1
# v1.50: Add owner to the node object.
# v1.51: Add description to the node object.
# v1.52: Add allocation API.
# v1.53: Add support for Smart NIC port
MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1
@ -144,6 +145,7 @@ MINOR_49_CONDUCTORS = 49
MINOR_50_NODE_OWNER = 50
MINOR_51_NODE_DESCRIPTION = 51
MINOR_52_ALLOCATION = 52
MINOR_53_PORT_SMARTNIC = 53
# When adding another version, update:
# - MINOR_MAX_VERSION
@ -151,7 +153,7 @@ MINOR_52_ALLOCATION = 52
# explanation of what changed in the new version
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
MINOR_MAX_VERSION = MINOR_52_ALLOCATION
MINOR_MAX_VERSION = MINOR_53_PORT_SMARTNIC
# String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -16,6 +16,7 @@ from oslo_log import log
from oslo_utils import uuidutils
import retrying
from ironic.api.controllers.v1 import types
from ironic.common import context as ironic_context
from ironic.common import exception
from ironic.common.i18n import _
@ -259,9 +260,10 @@ def add_ports_to_network(task, network_uuid, security_groups=None):
binding_profile = {'local_link_information':
[portmap[ironic_port.uuid]]}
body['port']['binding:profile'] = binding_profile
link_info = binding_profile['local_link_information'][0]
is_smart_nic = is_smartnic_port(ironic_port)
if is_smart_nic:
link_info = binding_profile['local_link_information'][0]
LOG.debug('Setting hostname as host_id in case of Smart NIC, '
'port %(port_id)s, hostname %(hostname)s',
{'port_id': ironic_port.uuid,
@ -504,11 +506,21 @@ def validate_port_info(node, port):
"in the nodes %(node)s port %(port)s",
{'node': node.uuid, 'port': port.uuid})
return False
if (port.is_smartnic and not types.locallinkconnectiontype
.validate_for_smart_nic(port.local_link_connection)):
LOG.error("Smart NIC port must have port_id and hostname in "
"local_link_connection, port: %s", port['id'])
return False
if (not port.is_smartnic and types.locallinkconnectiontype
.validate_for_smart_nic(port.local_link_connection)):
LOG.error("Only Smart NIC ports can have port_id and hostname "
"in local_link_connection, port: %s", port['id'])
return False
return True
def validate_agent(client, **kwargs):
def _validate_agent(client, **kwargs):
"""Check that the given neutron agent is alive
:param client: Neutron client
@ -670,7 +682,7 @@ def wait_for_host_agent(client, host_id, target_state='up'):
LOG.debug('Validating host %(host_id)s agent is %(status)s',
{'host_id': host_id,
'status': target_state})
is_alive = validate_agent(client, host=host_id)
is_alive = _validate_agent(client, host=host_id)
LOG.debug('Agent on host %(host_id)s is %(status)s',
{'host_id': host_id,
'status': 'up' if is_alive else 'down'})

View File

@ -131,7 +131,7 @@ RELEASE_MAPPING = {
}
},
'master': {
'api': '1.51',
'api': '1.53',
'rpc': '1.48',
'objects': {
'Allocation': ['1.0'],

View File

@ -351,6 +351,18 @@ class TestListPorts(test_api_base.BaseApiTest):
headers={api_base.Version.string: "1.34"})
self.assertNotIn('physical_network', data)
def test_hide_fields_in_newer_versions_is_smartnic(self):
port = obj_utils.create_test_port(self.context, node_id=self.node.id,
is_smartnic=True)
data = self.get_json(
'/ports/%s' % port.uuid,
headers={api_base.Version.string: "1.52"})
self.assertNotIn('is_smartnic', data)
data = self.get_json('/ports/%s' % port.uuid,
headers={api_base.Version.string: "1.53"})
self.assertTrue(data['is_smartnic'])
def test_get_collection_custom_fields(self):
fields = 'uuid,extra'
for i in range(3):
@ -436,6 +448,24 @@ class TestListPorts(test_api_base.BaseApiTest):
expect_errors=True)
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
def test_get_custom_fields_is_smartnic(self):
port = obj_utils.create_test_port(self.context, node_id=self.node.id,
is_smartnic=True)
fields = 'uuid,is_smartnic'
response = self.get_json(
'/ports/%s?fields=%s' % (port.uuid, fields),
headers={api_base.Version.string: "1.52"},
expect_errors=True)
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
response = self.get_json(
'/ports/%s?fields=%s' % (port.uuid, fields),
headers={api_base.Version.string: "1.53"})
# 'links' field is always retrieved in the response
# regardless of which fields are specified.
self.assertItemsEqual(['uuid', 'is_smartnic', 'links'], response)
def test_detail(self):
llc = {'switch_info': 'switch', 'switch_id': 'aa:bb:cc:dd:ee:ff',
'port_id': 'Gig0/1'}
@ -445,7 +475,8 @@ class TestListPorts(test_api_base.BaseApiTest):
portgroup_id=portgroup.id,
pxe_enabled=False,
local_link_connection=llc,
physical_network='physnet1')
physical_network='physnet1',
is_smartnic=True)
data = self.get_json(
'/ports/detail',
headers={api_base.Version.string: str(api_v1.max_version())}
@ -458,6 +489,7 @@ class TestListPorts(test_api_base.BaseApiTest):
self.assertIn('local_link_connection', data['ports'][0])
self.assertIn('portgroup_uuid', data['ports'][0])
self.assertIn('physical_network', data['ports'][0])
self.assertIn('is_smartnic', data['ports'][0])
# never expose the node_id and portgroup_id
self.assertNotIn('node_id', data['ports'][0])
self.assertNotIn('portgroup_id', data['ports'][0])
@ -1680,6 +1712,7 @@ class TestPost(test_api_base.BaseApiTest):
pdict.pop('pxe_enabled')
pdict.pop('extra')
pdict.pop('physical_network')
pdict.pop('is_smartnic')
headers = {api_base.Version.string: str(api_v1.min_version())}
response = self.post_json('/ports', pdict, headers=headers)
self.assertEqual('application/json', response.content_type)
@ -2071,6 +2104,7 @@ class TestPost(test_api_base.BaseApiTest):
pdict = post_get_test_port(pxe_enabled=False,
extra={'vif_port_id': 'foo'})
pdict.pop('physical_network')
pdict.pop('is_smartnic')
response = self.post_json('/ports', pdict, headers=headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.CREATED, response.status_int)
@ -2227,6 +2261,64 @@ class TestPost(test_api_base.BaseApiTest):
self.assertIn('maximum character', response.json['error_message'])
self.assertFalse(mock_create.called)
def test_create_port_with_is_smartnic(self, mock_create):
llc = {'hostname': 'host1', 'port_id': 'rep0-0'}
pdict = post_get_test_port(is_smartnic=True, node_uuid=self.node.uuid,
local_link_connection=llc)
response = self.post_json('/ports', pdict, headers=self.headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.CREATED, response.status_int)
mock_create.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY,
'test-topic')
self.assertTrue(response.json['is_smartnic'])
port = objects.Port.get(self.context, pdict['uuid'])
self.assertTrue(port.is_smartnic)
def test_create_port_with_is_smartnic_default_value(self, mock_create):
pdict = post_get_test_port(node_uuid=self.node.uuid)
response = self.post_json('/ports', pdict, headers=self.headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.CREATED, response.status_int)
mock_create.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY,
'test-topic')
self.assertFalse(response.json['is_smartnic'])
port = objects.Port.get(self.context, pdict['uuid'])
self.assertFalse(port.is_smartnic)
def test_create_port_with_is_smartnic_old_api_version(self, mock_create):
pdict = post_get_test_port(is_smartnic=True, node_uuid=self.node.uuid)
headers = {api_base.Version.string: '1.52'}
response = self.post_json('/ports', pdict,
headers=headers,
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
self.assertFalse(mock_create.called)
def test_create_port_with_is_smartnic_missing_hostname(self, mock_create):
llc = {'switch_info': 'switch',
'switch_id': 'aa:bb:cc:dd:ee:ff',
'port_id': 'Gig0/1'}
pdict = post_get_test_port(is_smartnic=True,
node_uuid=self.node.uuid,
local_link_connection=llc)
response = self.post_json('/ports', pdict,
headers=self.headers, expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
self.assertFalse(mock_create.called)
def test_create_port_with_is_smartnic_missing_port_id(self, mock_create):
llc = {'switch_info': 'switch',
'switch_id': 'aa:bb:cc:dd:ee:ff',
'hostname': 'host'}
pdict = post_get_test_port(is_smartnic=True,
node_uuid=self.node.uuid,
local_link_connection=llc)
response = self.post_json('/ports', pdict,
headers=self.headers, expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
self.assertFalse(mock_create.called)
@mock.patch.object(rpcapi.ConductorAPI, 'destroy_port')
class TestDelete(test_api_base.BaseApiTest):

View File

@ -323,14 +323,14 @@ class TestLocalLinkConnectionType(base.TestCase):
self.assertRaisesRegex(exception.Invalid, 'are invalid keys',
v.validate, value)
def test_local_link_connection_type_missing_mandatory_key(self):
def test_local_link_connection_type_missing_local_link_mandatory_key(self):
v = types.locallinkconnectiontype
value = {'switch_id': '0a:1b:2c:3d:4e:5f',
'switch_info': 'value3'}
self.assertRaisesRegex(exception.Invalid, 'Missing mandatory',
v.validate, value)
def test_local_link_connection_type_without_optional_key(self):
def test_local_link_connection_type_local_link_keys_mandatory(self):
v = types.locallinkconnectiontype
value = {'switch_id': '0a:1b:2c:3d:4e:5f',
'port_id': 'value2'}
@ -341,6 +341,34 @@ class TestLocalLinkConnectionType(base.TestCase):
value = {}
self.assertItemsEqual(value, v.validate(value))
def test_local_link_connection_type_smart_nic_keys_mandatory(self):
v = types.locallinkconnectiontype
value = {'port_id': 'rep0-0',
'hostname': 'hostname'}
self.assertTrue(v.validate_for_smart_nic(value))
self.assertTrue(v.validate(value))
def test_local_link_connection_type_smart_nic_keys_with_optional(self):
v = types.locallinkconnectiontype
value = {'port_id': 'rep0-0',
'hostname': 'hostname',
'switch_id': '0a:1b:2c:3d:4e:5f',
'switch_info': 'sw_info'}
self.assertTrue(v.validate_for_smart_nic(value))
self.assertTrue(v.validate(value))
def test_local_link_connection_type_smart_nic_keys_hostname_missing(self):
v = types.locallinkconnectiontype
value = {'port_id': 'rep0-0'}
self.assertFalse(v.validate_for_smart_nic(value))
self.assertRaises(exception.Invalid, v.validate, value)
def test_local_link_connection_type_smart_nic_keys_port_id_missing(self):
v = types.locallinkconnectiontype
value = {'hostname': 'hostname'}
self.assertFalse(v.validate_for_smart_nic(value))
self.assertRaises(exception.Invalid, v.validate, value)
@mock.patch("pecan.request", mock.Mock(version=mock.Mock(minor=10)))
class TestVifType(base.TestCase):

View File

@ -523,6 +523,13 @@ class TestApiUtils(base.TestCase):
mock_request.version.minor = 40
self.assertFalse(utils.allow_inspect_abort())
@mock.patch.object(pecan, 'request', spec_set=['version'])
def test_allow_port_is_smartnic(self, mock_request):
mock_request.version.minor = 53
self.assertTrue(utils.allow_port_is_smartnic())
mock_request.version.minor = 52
self.assertFalse(utils.allow_port_is_smartnic())
class TestNodeIdent(base.TestCase):

View File

@ -119,9 +119,6 @@ def port_post_data(**kw):
port.pop('version')
port.pop('node_id')
port.pop('portgroup_id')
# TODO(hamdyk): remove when port API can handle this attribute
port.pop('is_smartnic')
internal = port_controller.PortPatchType.internal_attrs()
return remove_internal(port, internal)

View File

@ -586,15 +586,57 @@ class TestNeutronNetworkActions(db_base.DbTestCase):
self.assertTrue(res)
self.assertFalse(log_mock.warning.called)
@mock.patch.object(neutron, 'LOG', autospec=True)
def test_validate_port_info_neutron_with_smartnic_and_link_info(
self, log_mock):
self.node.network_interface = 'neutron'
self.node.save()
llc = {'hostname': 'host1', 'port_id': 'rep0-0'}
port = object_utils.create_test_port(
self.context, node_id=self.node.id, uuid=uuidutils.generate_uuid(),
address='52:54:00:cf:2d:33', local_link_connection=llc,
is_smartnic=True)
res = neutron.validate_port_info(self.node, port)
self.assertTrue(res)
self.assertFalse(log_mock.error.called)
@mock.patch.object(neutron, 'LOG', autospec=True)
def test_validate_port_info_neutron_with_no_smartnic_and_link_info(
self, log_mock):
self.node.network_interface = 'neutron'
self.node.save()
llc = {'hostname': 'host1', 'port_id': 'rep0-0'}
port = object_utils.create_test_port(
self.context, node_id=self.node.id, uuid=uuidutils.generate_uuid(),
address='52:54:00:cf:2d:33', local_link_connection=llc,
is_smartnic=False)
res = neutron.validate_port_info(self.node, port)
self.assertFalse(res)
self.assertTrue(log_mock.error.called)
@mock.patch.object(neutron, 'LOG', autospec=True)
def test_validate_port_info_neutron_with_smartnic_and_no_link_info(
self, log_mock):
self.node.network_interface = 'neutron'
self.node.save()
llc = {'switch_id': 'switch', 'port_id': 'rep0-0'}
port = object_utils.create_test_port(
self.context, node_id=self.node.id, uuid=uuidutils.generate_uuid(),
address='52:54:00:cf:2d:33', local_link_connection=llc,
is_smartnic=True)
res = neutron.validate_port_info(self.node, port)
self.assertFalse(res)
self.assertTrue(log_mock.error.called)
def test_validate_agent_up(self):
self.client_mock.list_agents.return_value = {
'agents': [{'alive': True}]}
self.assertTrue(neutron.validate_agent(self.client_mock))
self.assertTrue(neutron._validate_agent(self.client_mock))
def test_validate_agent_down(self):
self.client_mock.list_agents.return_value = {
'agents': [{'alive': False}]}
self.assertFalse(neutron.validate_agent(self.client_mock))
self.assertFalse(neutron._validate_agent(self.client_mock))
def test_is_smartnic_port_true(self):
port = self.ports[0]
@ -605,19 +647,42 @@ class TestNeutronNetworkActions(db_base.DbTestCase):
port = self.ports[0]
self.assertFalse(neutron.is_smartnic_port(port))
@mock.patch.object(neutron, 'validate_agent')
@mock.patch.object(neutron, '_validate_agent')
@mock.patch.object(time, 'sleep')
def test_wait_for_host_agent_up(self, sleep_mock, validate_agent_mock):
def test_wait_for_host_agent_up_target_state_up(
self, sleep_mock, validate_agent_mock):
validate_agent_mock.return_value = True
neutron.wait_for_host_agent(self.client_mock, 'hostname')
self.assertTrue(neutron.wait_for_host_agent(
self.client_mock, 'hostname'))
sleep_mock.assert_not_called()
@mock.patch.object(neutron, 'validate_agent')
@mock.patch.object(neutron, '_validate_agent')
@mock.patch.object(time, 'sleep')
def test_wait_for_host_agent_down(self, sleep_mock, validate_agent_mock):
validate_agent_mock.side_effect = [False, True]
neutron.wait_for_host_agent(self.client_mock, 'hostname')
sleep_mock.assert_called_once()
def test_wait_for_host_agent_down_target_state_up(
self, sleep_mock, validate_agent_mock):
validate_agent_mock.return_value = False
self.assertRaises(exception.NetworkError,
neutron.wait_for_host_agent,
self.client_mock, 'hostname')
@mock.patch.object(neutron, '_validate_agent')
@mock.patch.object(time, 'sleep')
def test_wait_for_host_agent_up_target_state_down(
self, sleep_mock, validate_agent_mock):
validate_agent_mock.return_value = True
self.assertRaises(exception.NetworkError,
neutron.wait_for_host_agent,
self.client_mock, 'hostname', target_state='down')
@mock.patch.object(neutron, '_validate_agent')
@mock.patch.object(time, 'sleep')
def test_wait_for_host_agent_down_target_state_down(
self, sleep_mock, validate_agent_mock):
validate_agent_mock.return_value = False
self.assertTrue(
neutron.wait_for_host_agent(self.client_mock, 'hostname',
target_state='down'))
sleep_mock.assert_not_called()
@mock.patch.object(neutron, '_get_port_by_uuid')
@mock.patch.object(time, 'sleep')

View File

@ -0,0 +1,21 @@
---
features:
- |
Adds an ``is_smartnic`` field to the port object in REST API version
1.53.
``is_smartnic`` field indicates if this port is a Smart NIC port,
False by default. This field may be set by operator to use baremetal
nodes with Smart NICs as ironic nodes.
The REST API endpoints related to ports provide support for the
``is_smartnic`` field. The `ironic developer documentation
<https://docs.openstack.org/ironic/latest/admin/multitenancy.html>`_
provides information on how to configure and use Smart NIC ports.
upgrade:
- |
Adds an ``is_smartnic`` field to the port object in REST API version
1.53.
Upgrading to this release will set ``is_smartnic`` to False for all
ports.