Add network_type to port local_link_connection

Add network_type to the port objects local_link_connection field.
The network_type can be set to either managed or unmanaged. When
the type is unmanaged other fields are not required. Use
unmanaged when the neutron network_interface is required, but
the network is in fact a flat network where no actual switch
management is done.

Story: 2007315
Task: 39161
Change-Id: I00c5ea22a8163c27c9ce4470e3713c282d0eb131
This commit is contained in:
Harald Jensås 2020-03-26 21:28:13 +01:00
parent 6798715eaf
commit c40d221fca
9 changed files with 147 additions and 6 deletions

View File

@ -2,6 +2,15 @@
REST API Version History
========================
1.64 (Ussuri, master)
---------------------
Added the ``network_type`` to the port objects ``local_link_connection`` field.
The ``network_type`` can be set to either ``managed`` or ``unmanaged``. When the
type is ``unmanaged`` other fields are not required. Use ``unmanaged`` when the
neutron ``network_interface`` is required, but the network is in fact a flat
network where no actual switch management is done.
1.63 (Ussuri, master)
---------------------

View File

@ -440,6 +440,14 @@ class PortsController(rest.RestController):
if ('is_smartnic' in fields
and not api_utils.allow_port_is_smartnic()):
raise exception.NotAcceptable()
if ('local_link_connection/network_type' in fields
and not api_utils.allow_local_link_connection_network_type()):
raise exception.NotAcceptable()
if isinstance(fields, dict):
if (not api_utils.allow_local_link_connection_network_type()
and 'network_type' in fields.get('local_link_connection',
{}).keys()):
raise exception.NotAcceptable()
@METRICS.timer('PortsController.get_all')
@expose.expose(PortCollection, types.uuid_or_name, types.uuid,
@ -668,11 +676,10 @@ class PortsController(rest.RestController):
'baremetal:port:update', port_uuid)
context = api.request.context
fields_to_check = set()
for field in (self.advanced_net_fields
+ ['portgroup_uuid', 'physical_network',
'is_smartnic']):
'is_smartnic', 'local_link_connection/network_type']):
field_path = '/%s' % field
if (api_utils.get_patch_values(patch, field_path)
or api_utils.is_path_removed(patch, field_path)):

View File

@ -274,8 +274,9 @@ class LocalLinkConnectionType(wtypes.UserType):
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)
optional_fields = {'switch_info', 'network_type'}
valid_fields = set.union(optional_fields, *mandatory_fields_list)
valid_network_types = {'managed', 'unmanaged'}
@staticmethod
def validate(value):
@ -318,6 +319,25 @@ class LocalLinkConnectionType(wtypes.UserType):
if invalid:
raise exception.Invalid(_('%s are invalid keys') % (invalid))
# If network_type is 'unmanaged', this is a network with no switch
# management. i.e local_link_connection details are not required.
if 'network_type' in keys:
if (value['network_type'] not in
LocalLinkConnectionType.valid_network_types):
msg = _(
'Invalid network_type %(type)s, valid network_types are '
'%(valid_network_types)s.') % {
'type': value['network_type'],
'valid_network_types':
LocalLinkConnectionType.valid_network_types}
raise exception.Invalid(msg)
if (value['network_type'] == 'unmanaged'
and not (keys - {'network_type'})):
# Only valid network_type 'unmanaged' is set, no for further
# validation required.
return value
# Check any mandatory fields sets are present
for mandatory_set in LocalLinkConnectionType.mandatory_fields_list:
if mandatory_set <= keys:

View File

@ -1369,3 +1369,9 @@ def allow_allocation_owner():
def allow_agent_token():
"""Check if agent token is available."""
return api.request.version.minor >= versions.MINOR_62_AGENT_TOKEN
def allow_local_link_connection_network_type():
"""Check if network_type is allowed in ports link_local_connection"""
return (api.request.version.minor
>= versions.MINOR_64_LOCAL_LINK_CONNECTION_NETWORK_TYPE)

View File

@ -101,6 +101,7 @@ BASE_VERSION = 1
# v1.61: Add retired and retired_reason to the node object.
# v1.62: Add agent_token support for agent communication.
# v1.63: Add support for indicators
# v1.64: Add network_type to port.local_link_connection
MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1
@ -166,6 +167,7 @@ MINOR_60_ALLOCATION_OWNER = 60
MINOR_61_NODE_RETIRED = 61
MINOR_62_AGENT_TOKEN = 62
MINOR_63_INDICATORS = 63
MINOR_64_LOCAL_LINK_CONNECTION_NETWORK_TYPE = 64
# When adding another version, update:
# - MINOR_MAX_VERSION
@ -173,7 +175,7 @@ MINOR_63_INDICATORS = 63
# explanation of what changed in the new version
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
MINOR_MAX_VERSION = MINOR_63_INDICATORS
MINOR_MAX_VERSION = MINOR_64_LOCAL_LINK_CONNECTION_NETWORK_TYPE
# String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -214,7 +214,7 @@ RELEASE_MAPPING = {
}
},
'master': {
'api': '1.63',
'api': '1.64',
'rpc': '1.50',
'objects': {
'Allocation': ['1.1'],

View File

@ -1246,6 +1246,59 @@ class TestPatch(test_api_base.BaseApiTest):
self.assertTrue(response.json['error_message'])
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
def test_add_local_link_connection_network_type(self, mock_upd):
response = self.patch_json(
'/ports/%s' % self.port.uuid,
[{'path': '/local_link_connection/network_type',
'value': 'unmanaged', 'op': 'add'}],
headers={api_base.Version.string: '1.64'})
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
self.assertEqual(
'unmanaged',
response.json['local_link_connection']['network_type'])
self.assertTrue(mock_upd.called)
kargs = mock_upd.call_args[0][2]
self.assertEqual('unmanaged',
kargs.local_link_connection['network_type'])
def test_add_local_link_connection_network_type_old_api(self, mock_upd):
response = self.patch_json(
'/ports/%s' % self.port.uuid,
[{'path': '/local_link_connection/network_type',
'value': 'unmanaged', 'op': 'add'}],
headers={api_base.Version.string: '1.63'}, expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
def test_remove_local_link_connection_network_type(self, mock_upd):
llc = {'network_type': 'unmanaged'}
port = obj_utils.create_test_port(self.context,
node_id=self.node.id,
uuid=uuidutils.generate_uuid(),
address='bb:bb:bb:bb:bb:bb',
local_link_connection=llc)
llc.pop('network_type')
response = self.patch_json(
'/ports/%s' % port.uuid,
[{'path': '/local_link_connection/network_type', 'op': 'remove'}],
headers={api_base.Version.string: '1.64'})
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
self.assertTrue(mock_upd.called)
self.assertEqual(llc, response.json['local_link_connection'])
def test_remove_local_link_connection_network_type_old_api(self, mock_upd):
response = self.patch_json(
'/ports/%s' % self.port.uuid,
[{'path': '/local_link_connection/network_type', 'op': 'remove'}],
headers={api_base.Version.string: '1.63'}, expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
def test_set_pxe_enabled_false_old_api(self, mock_upd):
response = self.patch_json('/ports/%s' % self.port.uuid,
[{'path': '/pxe_enabled',
@ -2264,6 +2317,26 @@ class TestPost(test_api_base.BaseApiTest):
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
self.assertFalse(mock_create.called)
def test_create_port_with_network_type_in_llc(self, mock_create):
pdict = post_get_test_port(
local_link_connection={'network_type': 'unmanaged'})
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')
def test_create_port_with_network_type_in_llc_old_api_version(
self, mock_create):
headers = {api_base.Version.string: '1.63'}
pdict = post_get_test_port(
local_link_connection={'network_type': 'unmanaged'})
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_pxe_enabled_old_api_version(self, mock_create):
headers = {api_base.Version.string: '1.14'}
pdict = post_get_test_port(pxe_enabled=False)

View File

@ -365,6 +365,23 @@ class TestLocalLinkConnectionType(base.TestCase):
self.assertFalse(v.validate_for_smart_nic(value))
self.assertRaises(exception.Invalid, v.validate, value)
def test_local_link_connection_net_type_unmanaged(self):
v = types.locallinkconnectiontype
value = {'network_type': 'unmanaged'}
self.assertItemsEqual(value, v.validate(value))
def test_local_link_connection_net_type_unmanaged_combine_ok(self):
v = types.locallinkconnectiontype
value = {'network_type': 'unmanaged',
'switch_id': '0a:1b:2c:3d:4e:5f',
'port_id': 'rep0-0'}
self.assertItemsEqual(value, v.validate(value))
def test_local_link_connection_net_type_invalid(self):
v = types.locallinkconnectiontype
value = {'network_type': 'invalid'}
self.assertRaises(exception.Invalid, v.validate, value)
@mock.patch("ironic.api.request", mock.Mock(version=mock.Mock(minor=10)))
class TestVifType(base.TestCase):

View File

@ -0,0 +1,7 @@
---
features:
- |
To allow use of the ``neutron`` network interface in combination with
``flat`` provider networks where no actual switch management is done. The
`local_link_connection` field on ports is extended to support the
``network_type`` field.