Expose ports' physical network attribute in API

In change Ib22753aa6ae0fedce7fb9ecf63f135fda0185c5b the port data model
was updated to include a physical_network field, but this was not
exposed to the user by the REST API. This change exposes the
physical_network field in the REST API.

The port CRUD notification object has been updated to include the
physical_network field.

The API reference and user guide have been updated to include
information about the ports' physical network field.

The API microversion has been bumped to 1.34. During a rolling upgrade
from Ocata when the API service is pinned, the port physical network
field is hidden from API responses, and API requests including the field
are rejected.

Change-Id: I7023a1d6618608c867c31396fa677d3016ca493e
Partial-Bug: #1666009
This commit is contained in:
Mark Goddard 2017-06-01 15:36:58 +01:00
parent d9983f1eec
commit 02fff930fb
26 changed files with 627 additions and 68 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.31"
IRONIC_API_VERSION="1.34"
export OS_AUTH_TOKEN IRONIC_URL

View File

@ -18,6 +18,8 @@ List Ports by Node
Return a list of bare metal Ports associated with ``node_ident``.
API microversion 1.34 added the ``physical_network`` field.
Normal response code: 200
Error codes: TBD
@ -56,6 +58,8 @@ List detailed Ports by Node
Return a detailed list of bare metal Ports associated with ``node_ident``.
API microversion 1.34 added the ``physical_network`` field.
Normal response code: 200
Error codes: TBD
@ -83,6 +87,7 @@ Response
- node_uuid: node_uuid
- local_link_connection: local_link_connection
- pxe_enabled: pxe_enabled
- physical_network: physical_network
- internal_info: internal_info
- extra: extra
- created_at: created_at

View File

@ -22,6 +22,8 @@ List Ports by Portgroup
Return a list of bare metal Ports associated with ``portgroup_ident``.
API microversion 1.34 added the ``physical_network`` field.
Normal response code: 200
Error codes: 400,401,403,404
@ -60,6 +62,8 @@ List detailed Ports by Portgroup
Return a detailed list of bare metal Ports associated with ``portgroup_ident``.
API microversion 1.34 added the ``physical_network`` field.
Normal response code: 200
Error codes: 400,401,403,404
@ -86,6 +90,7 @@ Response
- node_uuid: node_uuid
- local_link_connection: local_link_connection
- pxe_enabled: pxe_enabled
- physical_network: physical_network
- internal_info: internal_info
- extra: extra
- portgroup_uuid: portgroup_uuid

View File

@ -39,6 +39,8 @@ fields.
API microversion 1.24 added the portgroup_uuid field.
API microversion 1.34 added the ``physical_network`` field.
Normal response code: 200
Request
@ -82,6 +84,8 @@ Creates a new Port resource.
This method requires a Node UUID and the physical hardware address for the Port
(MAC address in most cases).
``physical_network`` response field was added in API microversion 1.34.
Normal response code: 201
Request
@ -91,6 +95,7 @@ Request
- node_uuid: node_uuid
- address: port_address
- physical_network: physical_network
**Example Port creation request:**
@ -108,6 +113,7 @@ Response
- portgroup_uuid: portgroup_uuid
- local_link_connection: local_link_connection
- pxe_enabled: pxe_enabled
- physical_network: physical_network
- internal_info: internal_info
- extra: extra
- created_at: created_at
@ -134,6 +140,8 @@ will be used to filter results.
``portgroup`` query parameter and ``portgroup_uuid`` response field
were added in API microversion 1.24.
``physical_network`` response field was added in API microversion 1.34.
Normal response code: 200
Request
@ -162,6 +170,7 @@ Response
- portgroup_uuid: portgroup_uuid
- local_link_connection: local_link_connection
- pxe_enabled: pxe_enabled
- physical_network: physical_network
- internal_info: internal_info
- extra: extra
- created_at: created_at
@ -188,6 +197,8 @@ rather than the default set.
``portgroup`` query parameter and ``portgroup_uuid`` response field
were added in API microversion 1.24.
``physical_network`` response field was added in API microversion 1.34.
Normal response code: 200
Request
@ -209,6 +220,7 @@ Response
- portgroup_uuid: portgroup_uuid
- local_link_connection: local_link_connection
- pxe_enabled: pxe_enabled
- physical_network: physical_network
- internal_info: internal_info
- extra: extra
- created_at: created_at
@ -228,6 +240,8 @@ Update a Port
Update a Port.
API microversion 1.34 added the ``physical_network`` field.
Normal response code: 200
Request
@ -256,6 +270,7 @@ Response
- portgroup_uuid: portgroup_uuid
- local_link_connection: local_link_connection
- pxe_enabled: pxe_enabled
- physical_network: physical_network
- internal_info: internal_info
- extra: extra
- created_at: created_at

View File

@ -758,6 +758,13 @@ pg_ports:
in: body
required: true
type: array
physical_network:
description: |
The name of the physical network to which a port is connected. May be
empty. Added in API microversion 1.34.
in: body
required: true
type: string
port_address:
description: |
Physical hardware address of this network Port, typically the hardware

View File

@ -21,6 +21,7 @@
"switch_info": "switch1"
},
"node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d",
"physical_network": "physnet1",
"portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a",
"pxe_enabled": true,
"updated_at": "2016-08-18T22:28:49.653974+00:00",

View File

@ -6,5 +6,6 @@
"switch_id": "0a:1b:2c:3d:4e:5f",
"port_id": "Ethernet3/1",
"switch_info": "switch1"
}
},
"physical_network": "physnet1"
}

View File

@ -19,6 +19,7 @@
"switch_info": "switch1"
},
"node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d",
"physical_network": "physnet1",
"portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a",
"pxe_enabled": true,
"updated_at": null,

View File

@ -21,6 +21,7 @@
"switch_info": "switch1"
},
"node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d",
"physical_network": "physnet1",
"portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a",
"pxe_enabled": true,
"updated_at": null,

View File

@ -19,6 +19,7 @@
"switch_info": "switch1"
},
"node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d",
"physical_network": "physnet1",
"portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a",
"pxe_enabled": true,
"updated_at": "2016-08-18T22:28:49.653974+00:00",

View File

@ -21,6 +21,7 @@
"switch_info": "switch1"
},
"node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d",
"physical_network": "physnet1",
"portgroup_uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a",
"pxe_enabled": true,
"updated_at": "2016-08-18T22:28:49.653974+00:00",

View File

@ -14,6 +14,12 @@ nodes in a separate provisioning network. The result of this is that multiple
tenants can use nodes in an isolated fashion. However, this configuration does
not support trunk ports belonging to multiple networks.
Concepts
========
Network interfaces
------------------
Network interface is one of the driver interfaces that manages network
switching for nodes. There are 3 network interfaces available in
the Bare Metal service:
@ -28,6 +34,56 @@ the Bare Metal service:
the Networking service, while also separating tenant networks from the
provisioning and cleaning provider networks.
Local link connection
---------------------
The Bare Metal service allows ``local_link_connection`` information to be
associated with Bare Metal ports. This information is provided to the
Networking service's ML2 driver when a Virtual Interface (VIF) is attached. The
ML2 driver uses the information to plug the specified port to the tenant
network.
.. list-table:: ``local_link_connection`` fields
:header-rows: 1
* - Field
- Description
* - ``switch_id``
- 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.
* - ``switch_info``
- Optional. Used to distinguish different switch models or other
vendor-specific identifier. Some ML2 plugins may require this
field.
.. _multitenancy-physnets:
Physical networks
-----------------
A Bare Metal port may be associated with a physical network using its
``physical_network`` field. The Bare Metal service uses this information when
mapping between virtual ports in the Networking service and physical ports and
port groups in the Bare Metal service. A port's physical network field is
optional, and if not set then any virtual port may be mapped to that port,
provided that no free Bare Metal port with a suitable physical network
assignment exists.
The physical network of a port group is defined by the physical network of its
constituent ports. The Bare Metal service ensures that all ports in a port
group have the same value in their physical network field.
When attaching a virtual interface (VIF) to a node, the following ordered
criteria are used to select a suitable unattached port or port group:
* Require ports or port groups to not have a physical network or to have a
physical network that matches one of the VIF's allowed physical networks.
* Prefer ports and port groups that have a physical network to ports and
port groups that do not have a physical network.
* Prefer port groups to ports. Prefer ports with PXE enabled.
Configuring the Bare Metal service
==================================
@ -39,19 +95,28 @@ Bare Metal service.
Configuring nodes
=================
#. Multi-tenancy support was added in the 1.20 API version. The following
examples assume you are using python-ironicclient version 1.5.0 or higher.
They show the usage of both ``ironic`` and ``openstack baremetal`` commands.
#. Ensure that your python-ironicclient version and requested API version
are sufficient for your requirements.
* Multi-tenancy support was added in API version 1.20, and is supported by
python-ironicclient version 1.5.0 or higher.
* 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.
The following examples assume you are using python-ironicclient version
1.15.0 or higher. They show the usage of both ``ironic`` and ``openstack
baremetal`` commands.
If you're going to use ``ironic`` command, set the following variable in
your shell environment::
export IRONIC_API_VERSION=1.20
export IRONIC_API_VERSION=<API version>
If you're using ironic client plugin for openstack client via
``openstack baremetal`` commands, export the following variable::
export OS_BAREMETAL_API_VERSION=1.20
export OS_BAREMETAL_API_VERSION=<API version>
#. The node's ``network_interface`` field should be set to a valid network
interface. Valid interfaces are listed in the
@ -86,39 +151,21 @@ Configuring nodes
openstack baremetal node set $NODE_UUID_OR_NAME \
--network-interface neutron
#. The Bare Metal service provides the ``local_link_connection`` information to
the Networking service's ML2 driver. The ML2 driver uses that information to
plug the specified port to the tenant network.
.. list-table:: ``local_link_connection`` fields
:header-rows: 1
* - Field
- Description
* - ``switch_id``
- 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.
* - ``switch_info``
- Optional. Used to distinguish different switch models or other
vendor-specific identifier. Some ML2 plugins may require this
field.
Create a port as follows:
#. Create a port as follows:
- ``ironic`` command::
ironic port-create -a $HW_MAC_ADDRESS -n $NODE_UUID \
-l switch_id=$SWITCH_MAC_ADDRESS -l switch_info=$SWITCH_HOSTNAME \
-l port_id=$SWITCH_PORT --pxe-enabled true
-l port_id=$SWITCH_PORT --pxe-enabled true --physical-network physnet1
- ``openstack`` command::
openstack baremetal port create $HW_MAC_ADDRESS --node $NODE_UUID \
--local-link-connection switch_id=$SWITCH_MAC_ADDRESS \
--local-link-connection switch_info=$SWITCH_HOSTNAME \
--local-link-connection port_id=$SWITCH_PORT --pxe-enabled true
--local-link-connection port_id=$SWITCH_PORT --pxe-enabled true \
--physical-network physnet1
#. Check the port configuration:

View File

@ -198,13 +198,14 @@ Example of port CRUD notification::
"payload":{
"ironic_object.namespace":"ironic",
"ironic_object.name":"PortCRUDPayload",
"ironic_object.version":"1.1",
"ironic_object.version":"1.2",
"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": {},
"local_link_connection": {},
"physical_network": "physnet1",
"portgroup_uuid": "bd2f385e-c51c-4752-82d1-7a9ec2c25f24",
"pxe_enabled": True,
"updated_at": "2016-03-27T20:41:03+00:00",

View File

@ -27,6 +27,19 @@ members to be used by themselves, you need to set port group's
``standalone_ports_supported`` value to be ``False`` in ironic, as it is
``True`` by default.
Physical networks
-----------------
If any port in a port group has a physical network, then all ports in
that port group must have the same physical network.
In order to change the physical network of the ports in a port group, all ports
must first be removed from the port group, before changing their physical
networks (to the same value), then adding them back to the port group.
See :ref:`physical networks <multitenancy-physnets>` for further information on
using physical networks in the Bare Metal service.
Port groups configuration in the Bare Metal service
---------------------------------------------------

View File

@ -2,6 +2,11 @@
REST API Version History
========================
**1.34** (Pike)
Adds a ``physical_network`` field to the port object. All ports in a
portgroup must have the same value in their ``physical_network`` field.
**1.33** (Pike)
Added ``storage_interface`` field to the node object to allow getting and

View File

@ -54,6 +54,9 @@ def hide_fields_in_newer_versions(obj):
# if requested version is < 1.24, hide portgroup_uuid field
if not api_utils.allow_portgroups_subcontrollers():
obj.portgroup_uuid = wsme.Unset
# if requested version is < 1.34, hide physical_network field.
if not api_utils.allow_port_physical_network():
obj.physical_network = wsme.Unset
class Port(base.APIBase):
@ -145,6 +148,9 @@ class Port(base.APIBase):
local_link_connection = types.locallinkconnectiontype
"""The port binding profile for the port"""
physical_network = wtypes.StringType(max_length=64)
"""The name of the physical network to which this port is connected."""
links = wsme.wsattr([link.Link], readonly=True)
"""A list containing a self link and associated port links"""
@ -224,7 +230,8 @@ class Port(base.APIBase):
pxe_enabled=True,
local_link_connection={
'switch_info': 'host', 'port_id': 'Gig0/1',
'switch_id': 'aa:bb:cc:dd:ee:ff'})
'switch_id': 'aa:bb:cc:dd:ee:ff'},
physical_network='physnet1')
# NOTE(lucasagomes): node_uuid getter() method look at the
# _node_uuid variable
sample._node_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
@ -371,6 +378,9 @@ class PortsController(rest.RestController):
if ('portgroup_uuid' in fields and not
api_utils.allow_portgroups_subcontrollers()):
raise exception.NotAcceptable()
if ('physical_network' in fields and not
api_utils.allow_port_physical_network()):
raise exception.NotAcceptable()
@METRICS.timer('PortsController.get_all')
@expose.expose(PortCollection, types.uuid_or_name, types.uuid,
@ -497,6 +507,7 @@ class PortsController(rest.RestController):
raise exception.OperationNotPermitted()
api_utils.check_allow_specify_fields(fields)
self._check_allowed_port_fields(fields)
rpc_port = objects.Port.get_by_uuid(pecan.request.context, port_uuid)
return Port.convert_with_links(rpc_port, fields=fields)
@ -523,6 +534,7 @@ class PortsController(rest.RestController):
vif = extra.get('vif_port_id') if extra else None
if vif:
common_utils.warn_about_deprecated_extra_vif_port_id()
if (pdict.get('portgroup_uuid') and
(pdict.get('pxe_enabled') or vif)):
rpc_pg = objects.Portgroup.get_by_uuid(context,
@ -582,7 +594,8 @@ class PortsController(rest.RestController):
raise exception.OperationNotPermitted()
fields_to_check = set()
for field in self.advanced_net_fields + ['portgroup_uuid']:
for field in (self.advanced_net_fields +
['portgroup_uuid', 'physical_network']):
field_path = '/%s' % field
if (api_utils.get_patch_values(patch, field_path) or
api_utils.is_path_removed(patch, field_path)):

View File

@ -568,6 +568,19 @@ def allow_storage_interface():
versions.MINOR_33_STORAGE_INTERFACE)
def allow_port_physical_network():
"""Check if port physical network field is allowed.
Version 1.34 of the API added the physical network field to the port
object. We also check whether the target version of the Port object
supports the physical_network field as this may not be the case during a
rolling upgrade.
"""
return ((pecan.request.version.minor >=
versions.MINOR_34_PORT_PHYSICAL_NETWORK) and
objects.Port.supports_physical_network())
def get_controller_reserved_names(cls):
"""Get reserved names for a given controller.

View File

@ -64,6 +64,7 @@ BASE_VERSION = 1
# v1.31: Add dynamic interfaces fields to node.
# v1.32: Add volume support.
# v1.33: Add node storage interface
# v1.34: Add physical network field to port.
MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1
@ -99,11 +100,12 @@ MINOR_30_DYNAMIC_DRIVERS = 30
MINOR_31_DYNAMIC_INTERFACES = 31
MINOR_32_VOLUME = 32
MINOR_33_STORAGE_INTERFACE = 33
MINOR_34_PORT_PHYSICAL_NETWORK = 34
# When adding another version, update MINOR_MAX_VERSION and also update
# doc/source/dev/webapi-version-history.rst with a detailed explanation of
# what the version has changed.
MINOR_MAX_VERSION = MINOR_33_STORAGE_INTERFACE
MINOR_MAX_VERSION = MINOR_34_PORT_PHYSICAL_NETWORK
# String representations of the minor and maximum versions
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -154,7 +154,8 @@ class IronicObject(object_base.VersionedObject):
self.VERSION != self.__class__.VERSION):
self.VERSION = target_version
def get_target_version(self):
@classmethod
def get_target_version(cls):
"""Returns the target version for this object.
This is the version in which the object should be manipulated, e.g.
@ -166,27 +167,43 @@ class IronicObject(object_base.VersionedObject):
"""
pin = CONF.pin_release_version
if not pin:
return self.__class__.VERSION
return cls.VERSION
version_manifest = versions.RELEASE_MAPPING[pin]['objects']
pinned_version = version_manifest.get(self.obj_name())
pinned_version = version_manifest.get(cls.obj_name())
if pinned_version:
if not versionutils.is_compatible(pinned_version,
self.__class__.VERSION):
cls.VERSION):
LOG.error(
'For object "%(objname)s", the target version '
'"%(target)s" is not compatible with its supported '
'version "%(support)s". The value ("%(pin)s") of the '
'"pin_release_version" configuration option may be '
'incorrect.',
{'objname': self.obj_name(), 'target': pinned_version,
'support': self.__class__.VERSION, 'pin': pin})
{'objname': cls.obj_name(), 'target': pinned_version,
'support': cls.VERSION, 'pin': pin})
raise ovo_exception.IncompatibleObjectVersion(
objname=self.obj_name(), objver=pinned_version,
supported=self.__class__.VERSION)
objname=cls.obj_name(), objver=pinned_version,
supported=cls.VERSION)
return pinned_version
return self.__class__.VERSION
return cls.VERSION
@classmethod
def supports_version(cls, version):
"""Return whether this object supports a particular version.
Check the requested version against the object's target version. The
target version may not be the latest version during an upgrade, when
object versions are pinned.
:param version: A tuple representing the version to check
:returns: Whether the version is supported
:raises: ovo_exception.IncompatibleObjectVersion
"""
target_version = cls.get_target_version()
target_version = versionutils.convert_version_to_tuple(target_version)
return target_version >= version
def _set_from_db_object(self, context, db_object, fields=None):
"""Sets object fields.

View File

@ -299,6 +299,15 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat):
self.obj_refresh(current)
self.obj_reset_changes()
@classmethod
def supports_physical_network(cls):
"""Return whether the physical_network field is supported.
:returns: Whether the physical_network field is supported
:raises: ovo_exception.IncompatibleObjectVersion
"""
return cls.supports_version((1, 7))
@base.IronicObjectRegistry.register
class PortCRUDNotification(notification.NotificationBase):
@ -315,13 +324,15 @@ class PortCRUDNotification(notification.NotificationBase):
class PortCRUDPayload(notification.NotificationPayloadBase):
# Version 1.0: Initial version
# Version 1.1: Add "portgroup_uuid" field
VERSION = '1.1'
# Version 1.2: Add "physical_network" field
VERSION = '1.2'
SCHEMA = {
'address': ('port', 'address'),
'extra': ('port', 'extra'),
'local_link_connection': ('port', 'local_link_connection'),
'pxe_enabled': ('port', 'pxe_enabled'),
'physical_network': ('port', 'physical_network'),
'created_at': ('port', 'created_at'),
'updated_at': ('port', 'updated_at'),
'uuid': ('port', 'uuid')
@ -335,6 +346,7 @@ class PortCRUDPayload(notification.NotificationPayloadBase):
'pxe_enabled': object_fields.BooleanField(nullable=True),
'node_uuid': object_fields.UUIDField(),
'portgroup_uuid': object_fields.UUIDField(nullable=True),
'physical_network': object_fields.StringField(nullable=True),
'created_at': object_fields.DateTimeField(nullable=True),
'updated_at': object_fields.DateTimeField(nullable=True),
'uuid': object_fields.UUIDField()

View File

@ -120,8 +120,6 @@ def port_post_data(**kw):
port.pop('version')
port.pop('node_id')
port.pop('portgroup_id')
# NOTE(mgoddard): Physical network is not yet supported by the REST API.
port.pop('physical_network')
internal = port_controller.PortPatchType.internal_attrs()
return remove_internal(port, internal)

View File

@ -34,6 +34,7 @@ from ironic.api.controllers.v1 import versions
from ironic.common import exception
from ironic.common import utils as common_utils
from ironic.conductor import rpcapi
from ironic import objects
from ironic.objects import fields as obj_fields
from ironic.tests import base
from ironic.tests.unit.api import base as test_api_base
@ -75,6 +76,7 @@ class TestPortObject(base.TestCase):
self.assertEqual(wtypes.Unset, port.extra)
@mock.patch.object(api_utils, 'allow_port_physical_network', autospec=True)
@mock.patch.object(api_utils, 'allow_portgroups_subcontrollers', autospec=True)
@mock.patch.object(api_utils, 'allow_port_advanced_net_fields', autospec=True)
class TestPortsController__CheckAllowedPortFields(base.TestCase):
@ -84,14 +86,17 @@ class TestPortsController__CheckAllowedPortFields(base.TestCase):
self.controller = api_port.PortsController()
def test__check_allowed_port_fields_none(self, mock_allow_port,
mock_allow_portgroup):
mock_allow_portgroup,
mock_allow_physnet):
self.assertIsNone(
self.controller._check_allowed_port_fields(None))
self.assertFalse(mock_allow_port.called)
self.assertFalse(mock_allow_portgroup.called)
self.assertFalse(mock_allow_physnet.called)
def test__check_allowed_port_fields_empty(self, mock_allow_port,
mock_allow_portgroup):
mock_allow_portgroup,
mock_allow_physnet):
for v in (True, False):
mock_allow_port.return_value = v
self.assertIsNone(
@ -99,9 +104,11 @@ class TestPortsController__CheckAllowedPortFields(base.TestCase):
mock_allow_port.assert_called_once_with()
mock_allow_port.reset_mock()
self.assertFalse(mock_allow_portgroup.called)
self.assertFalse(mock_allow_physnet.called)
def test__check_allowed_port_fields_not_allow(self, mock_allow_port,
mock_allow_portgroup):
mock_allow_portgroup,
mock_allow_physnet):
mock_allow_port.return_value = False
for field in api_port.PortsController.advanced_net_fields:
self.assertRaises(exception.NotAcceptable,
@ -110,9 +117,11 @@ class TestPortsController__CheckAllowedPortFields(base.TestCase):
mock_allow_port.assert_called_once_with()
mock_allow_port.reset_mock()
self.assertFalse(mock_allow_portgroup.called)
self.assertFalse(mock_allow_physnet.called)
def test__check_allowed_port_fields_allow(self, mock_allow_port,
mock_allow_portgroup):
mock_allow_portgroup,
mock_allow_physnet):
mock_allow_port.return_value = True
for field in api_port.PortsController.advanced_net_fields:
self.assertIsNone(
@ -120,9 +129,10 @@ class TestPortsController__CheckAllowedPortFields(base.TestCase):
mock_allow_port.assert_called_once_with()
mock_allow_port.reset_mock()
self.assertFalse(mock_allow_portgroup.called)
self.assertFalse(mock_allow_physnet.called)
def test__check_allowed_port_fields_portgroup_not_allow(
self, mock_allow_port, mock_allow_portgroup):
self, mock_allow_port, mock_allow_portgroup, mock_allow_physnet):
mock_allow_port.return_value = True
mock_allow_portgroup.return_value = False
self.assertRaises(exception.NotAcceptable,
@ -130,15 +140,38 @@ class TestPortsController__CheckAllowedPortFields(base.TestCase):
['portgroup_uuid'])
mock_allow_port.assert_called_once_with()
mock_allow_portgroup.assert_called_once_with()
self.assertFalse(mock_allow_physnet.called)
def test__check_allowed_port_fields_portgroup_allow(
self, mock_allow_port, mock_allow_portgroup):
self, mock_allow_port, mock_allow_portgroup, mock_allow_physnet):
mock_allow_port.return_value = True
mock_allow_portgroup.return_value = True
self.assertIsNone(
self.controller._check_allowed_port_fields(['portgroup_uuid']))
mock_allow_port.assert_called_once_with()
mock_allow_portgroup.assert_called_once_with()
self.assertFalse(mock_allow_physnet.called)
def test__check_allowed_port_fields_physnet_not_allow(
self, mock_allow_port, mock_allow_portgroup, mock_allow_physnet):
mock_allow_port.return_value = True
mock_allow_physnet.return_value = False
self.assertRaises(exception.NotAcceptable,
self.controller._check_allowed_port_fields,
['physical_network'])
mock_allow_port.assert_called_once_with()
self.assertFalse(mock_allow_portgroup.called)
mock_allow_physnet.assert_called_once_with()
def test__check_allowed_port_fields_physnet_allow(
self, mock_allow_port, mock_allow_portgroup, mock_allow_physnet):
mock_allow_port.return_value = True
mock_allow_physnet.return_value = True
self.assertIsNone(
self.controller._check_allowed_port_fields(['physical_network']))
mock_allow_port.assert_called_once_with()
self.assertFalse(mock_allow_portgroup.called)
mock_allow_physnet.assert_called_once_with()
class TestListPorts(test_api_base.BaseApiTest):
@ -206,6 +239,60 @@ class TestListPorts(test_api_base.BaseApiTest):
headers={api_base.Version.string: "1.18"})
self.assertEqual({"foo": "bar"}, data['internal_info'])
def test_hide_fields_in_newer_versions_advanced_net(self):
llc = {'switch_info': 'switch', 'switch_id': 'aa:bb:cc:dd:ee:ff',
'port_id': 'Gig0/1'}
port = obj_utils.create_test_port(self.context, node_id=self.node.id,
pxe_enabled=True,
local_link_connection=llc)
data = self.get_json(
'/ports/%s' % port.uuid,
headers={api_base.Version.string: "1.18"})
self.assertNotIn('pxe_enabled', data)
self.assertNotIn('local_link_connection', data)
data = self.get_json('/ports/%s' % port.uuid,
headers={api_base.Version.string: "1.19"})
self.assertTrue(data['pxe_enabled'])
self.assertEqual(llc, data['local_link_connection'])
def test_hide_fields_in_newer_versions_portgroup_uuid(self):
portgroup = obj_utils.create_test_portgroup(self.context,
node_id=self.node.id)
port = obj_utils.create_test_port(self.context, node_id=self.node.id,
portgroup_id=portgroup.id)
data = self.get_json(
'/ports/%s' % port.uuid,
headers={api_base.Version.string: "1.23"})
self.assertNotIn('portgroup_uuid', data)
data = self.get_json('/ports/%s' % port.uuid,
headers={api_base.Version.string: "1.24"})
self.assertEqual(portgroup.uuid, data['portgroup_uuid'])
def test_hide_fields_in_newer_versions_physical_network(self):
port = obj_utils.create_test_port(self.context, node_id=self.node.id,
physical_network='physnet1')
data = self.get_json(
'/ports/%s' % port.uuid,
headers={api_base.Version.string: "1.33"})
self.assertNotIn('physical_network', data)
data = self.get_json('/ports/%s' % port.uuid,
headers={api_base.Version.string: "1.34"})
self.assertEqual("physnet1", data['physical_network'])
@mock.patch.object(objects.Port, 'supports_physical_network')
def test_hide_fields_in_newer_versions_physical_network_upgrade(self,
mock_spn):
mock_spn.return_value = False
port = obj_utils.create_test_port(self.context, node_id=self.node.id,
physical_network='physnet1')
data = self.get_json(
'/ports/%s' % port.uuid,
headers={api_base.Version.string: "1.34"})
self.assertNotIn('physical_network', data)
def test_get_collection_custom_fields(self):
fields = 'uuid,extra'
for i in range(3):
@ -243,6 +330,34 @@ class TestListPorts(test_api_base.BaseApiTest):
expect_errors=True)
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
def test_get_custom_fields_physical_network(self):
port = obj_utils.create_test_port(self.context, node_id=self.node.id,
physical_network='physnet1')
fields = 'uuid,physical_network'
response = self.get_json(
'/ports/%s?fields=%s' % (port.uuid, fields),
headers={api_base.Version.string: "1.33"},
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.34"})
# We always append "links".
self.assertItemsEqual(['uuid', 'physical_network', 'links'], response)
@mock.patch.object(objects.Port, 'supports_physical_network')
def test_get_custom_fields_physical_network_upgrade(self, mock_spn):
mock_spn.return_value = False
port = obj_utils.create_test_port(self.context, node_id=self.node.id,
physical_network='physnet1')
fields = 'uuid,physical_network'
response = self.get_json(
'/ports/%s?fields=%s' % (port.uuid, fields),
headers={api_base.Version.string: "1.34"},
expect_errors=True)
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
def test_detail(self):
llc = {'switch_info': 'switch', 'switch_id': 'aa:bb:cc:dd:ee:ff',
'port_id': 'Gig0/1'}
@ -251,7 +366,8 @@ class TestListPorts(test_api_base.BaseApiTest):
port = obj_utils.create_test_port(self.context, node_id=self.node.id,
portgroup_id=portgroup.id,
pxe_enabled=False,
local_link_connection=llc)
local_link_connection=llc,
physical_network='physnet1')
data = self.get_json(
'/ports/detail',
headers={api_base.Version.string: str(api_v1.MAX_VER)}
@ -263,12 +379,10 @@ class TestListPorts(test_api_base.BaseApiTest):
self.assertIn('pxe_enabled', data['ports'][0])
self.assertIn('local_link_connection', data['ports'][0])
self.assertIn('portgroup_uuid', data['ports'][0])
self.assertIn('physical_network', 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])
# NOTE(mgoddard): The physical network attribute is not yet exposed by
# the API.
self.assertNotIn('physical_network', data['ports'][0])
def test_detail_against_single(self):
port = obj_utils.create_test_port(self.context, node_id=self.node.id)
@ -584,6 +698,33 @@ class TestPatch(test_api_base.BaseApiTest):
self.mock_gtf.return_value = 'test-topic'
self.addCleanup(p.stop)
def _test_success(self, mock_upd, patch, version):
# Helper to test an update to a port that is expected to succeed at a
# given API version.
mock_upd.return_value = self.port
headers = {api_base.Version.string: version}
response = self.patch_json('/ports/%s' % self.port.uuid,
patch,
headers=headers)
self.assertEqual(http_client.OK, response.status_code)
self.assertTrue(mock_upd.called)
self.assertEqual(self.port.id, mock_upd.call_args[0][1].id)
return response
def _test_old_api_version(self, mock_upd, patch, version):
# Helper to test an update to a port affecting a field that is not
# available in the specified API version.
headers = {api_base.Version.string: version}
response = self.patch_json('/ports/%s' % self.port.uuid,
patch,
expect_errors=True,
headers=headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
self.assertFalse(mock_upd.called)
@mock.patch.object(notification_utils, '_emit_api_notification')
def test_update_byid(self, mock_notify, mock_upd):
extra = {'foo': 'bar'}
@ -1047,6 +1188,133 @@ class TestPatch(test_api_base.BaseApiTest):
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
self.assertFalse(mock_upd.called)
def _test_physical_network_success(self, mock_upd, patch,
expected_physical_network):
# Helper to test an update to a port's physical_network that is
# expected to succeed at API version 1.34.
self.port.physical_network = expected_physical_network
response = self._test_success(mock_upd, patch, '1.34')
self.assertEqual(expected_physical_network,
response.json['physical_network'])
# TODO(mgoddard): Add this when mock_upd has been modified to save the
# port to the DB.
# self.port.refresh()
# self.assertEqual(expected_physical_network,
# self.port.physical_network)
def test_add_physical_network(self, mock_upd):
physical_network = 'physnet1'
patch = [{'path': '/physical_network',
'value': physical_network,
'op': 'add'}]
self._test_physical_network_success(mock_upd, patch, physical_network)
def test_replace_physical_network(self, mock_upd):
self.port.physical_network = 'physnet1'
self.port.save()
new_physical_network = 'physnet2'
patch = [{'path': '/physical_network',
'value': new_physical_network,
'op': 'replace'}]
self._test_physical_network_success(mock_upd, patch,
new_physical_network)
def test_remove_physical_network(self, mock_upd):
self.port.physical_network = 'physnet1'
self.port.save()
patch = [{'path': '/physical_network', 'op': 'remove'}]
self._test_physical_network_success(mock_upd, patch, None)
def _test_physical_network_old_api_version(self, mock_upd, patch,
expected_physical_network):
# Helper to test an update to a port's physical network that is
# expected to fail at API version 1.33.
self._test_old_api_version(mock_upd, patch, '1.33')
self.port.refresh()
self.assertEqual(expected_physical_network, self.port.physical_network)
def test_add_physical_network_old_api_version(self, mock_upd):
patch = [{'path': '/physical_network',
'value': 'physnet1',
'op': 'add'}]
self._test_physical_network_old_api_version(mock_upd, patch, None)
def test_replace_physical_network_old_api_version(self, mock_upd):
self.port.physical_network = 'physnet1'
self.port.save()
patch = [{'path': '/physical_network',
'value': 'physnet2',
'op': 'replace'}]
self._test_physical_network_old_api_version(mock_upd, patch,
'physnet1')
def test_remove_physical_network_old_api_version(self, mock_upd):
self.port.physical_network = 'physnet1'
self.port.save()
patch = [{'path': '/physical_network', 'op': 'remove'}]
self._test_physical_network_old_api_version(mock_upd, patch,
'physnet1')
@mock.patch.object(objects.Port, 'supports_physical_network')
def _test_physical_network_upgrade(self, mock_upd, patch,
expected_physical_network, mock_spn):
# Helper to test an update to a port's physical network that is
# expected to fail at API version 1.34 while the API service is pinned
# to the Ocata release.
mock_spn.return_value = False
self._test_old_api_version(mock_upd, patch, '1.34')
self.port.refresh()
self.assertEqual(expected_physical_network, self.port.physical_network)
def test_add_physical_network_upgrade(self, mock_upd):
patch = [{'path': '/physical_network',
'value': 'physnet1',
'op': 'add'}]
self._test_physical_network_upgrade(mock_upd, patch, None)
def test_replace_physical_network_upgrade(self, mock_upd):
self.port.physical_network = 'physnet1'
self.port.save()
patch = [{'path': '/physical_network',
'value': 'physnet2',
'op': 'replace'}]
self._test_physical_network_upgrade(mock_upd, patch, 'physnet1')
def test_remove_physical_network_upgrade(self, mock_upd):
self.port.physical_network = 'physnet1'
self.port.save()
patch = [{'path': '/physical_network', 'op': 'remove'}]
self._test_physical_network_upgrade(mock_upd, patch, 'physnet1')
def test_invalid_physnet_non_text(self, mock_upd):
physnet = 1234
headers = {api_base.Version.string: versions.MAX_VERSION_STRING}
response = self.patch_json('/ports/%s' % self.port.uuid,
[{'path': '/physical_network',
'value': physnet,
'op': 'replace'}],
expect_errors=True,
headers=headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
self.assertIn('should be string', response.json['error_message'])
def test_invalid_physnet_too_long(self, mock_upd):
physnet = 'p' * 65
headers = {api_base.Version.string: versions.MAX_VERSION_STRING}
response = self.patch_json('/ports/%s' % self.port.uuid,
[{'path': '/physical_network',
'value': physnet,
'op': 'replace'}],
expect_errors=True,
headers=headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
self.assertIn('maximum character', response.json['error_message'])
def test_portgroups_subresource_patch(self, mock_upd):
portgroup = obj_utils.create_test_portgroup(self.context,
node_id=self.node.id)
@ -1124,6 +1392,7 @@ class TestPost(test_api_base.BaseApiTest):
pdict.pop('local_link_connection')
pdict.pop('pxe_enabled')
pdict.pop('extra')
pdict.pop('physical_network')
headers = {api_base.Version.string: str(api_v1.MIN_VER)}
response = self.post_json('/ports', pdict, headers=headers)
self.assertEqual('application/json', response.content_type)
@ -1436,6 +1705,42 @@ 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_physical_network(self, mock_create):
physical_network = 'physnet1'
pdict = post_get_test_port(
physical_network=physical_network,
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.assertEqual(physical_network, response.json['physical_network'])
port = objects.Port.get(self.context, pdict['uuid'])
self.assertEqual(physical_network, port.physical_network)
def test_create_port_with_physical_network_old_api_version(self,
mock_create):
headers = {api_base.Version.string: '1.33'}
pdict = post_get_test_port(physical_network='physnet1')
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)
@mock.patch.object(objects.Port, 'supports_physical_network')
def test_create_port_with_physical_network_upgrade(self, mock_spn,
mock_create):
mock_spn.return_value = False
pdict = post_get_test_port(physical_network='physnet1')
response = self.post_json('/ports', pdict, headers=self.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_portgroups_subresource_post(self, mock_create):
headers = {api_base.Version.string: '1.24'}
pdict = post_get_test_port()
@ -1566,6 +1871,26 @@ class TestPost(test_api_base.BaseApiTest):
standalone_ports=False,
http_status=http_client.CONFLICT)
def test_create_port_invalid_physnet_non_text(self, mock_create):
physnet = 1234
pdict = post_get_test_port(physical_network=physnet)
response = self.post_json('/ports', pdict, expect_errors=True,
headers=self.headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
self.assertIn('should be string', response.json['error_message'])
self.assertFalse(mock_create.called)
def test_create_port_invalid_physnet_too_long(self, mock_create):
physnet = 'p' * 65
pdict = post_get_test_port(physical_network=physnet)
response = self.post_json('/ports', pdict, expect_errors=True,
headers=self.headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
self.assertIn('maximum character', response.json['error_message'])
self.assertFalse(mock_create.called)
@mock.patch.object(rpcapi.ConductorAPI, 'destroy_port')
class TestDelete(test_api_base.BaseApiTest):

View File

@ -426,6 +426,24 @@ class TestApiUtils(base.TestCase):
mock_request.version.minor = 32
self.assertFalse(utils.allow_storage_interface())
@mock.patch.object(pecan, 'request', spec_set=['version'])
@mock.patch.object(objects.Port, 'supports_physical_network')
def test_allow_port_physical_network_no_pin(self, mock_spn, mock_request):
mock_spn.return_value = True
mock_request.version.minor = 34
self.assertTrue(utils.allow_port_physical_network())
mock_request.version.minor = 33
self.assertFalse(utils.allow_port_physical_network())
@mock.patch.object(pecan, 'request', spec_set=['version'])
@mock.patch.object(objects.Port, 'supports_physical_network')
def test_allow_port_physical_network_pin(self, mock_spn, mock_request):
mock_spn.return_value = False
mock_request.version.minor = 34
self.assertFalse(utils.allow_port_physical_network())
mock_request.version.minor = 33
self.assertFalse(utils.allow_port_physical_network())
class TestNodeIdent(base.TestCase):

View File

@ -435,7 +435,7 @@ class _TestObject(object):
self.assertFalse(mock_convert.called)
@mock.patch.object(MyObj, 'convert_to_version', autospec=True)
@mock.patch.object(base.IronicObject, 'get_target_version', autospec=True)
@mock.patch.object(base.IronicObject, 'get_target_version')
def test_do_version_changes_for_db_pinned(self, mock_target_version,
mock_convert):
# obj is same version as pinned, no conversion done
@ -452,10 +452,10 @@ class _TestObject(object):
self.assertEqual({'foo': 123, 'bar': 'test', 'version': '1.4'},
changes)
self.assertEqual('1.4', obj.VERSION)
mock_target_version.assert_called_with(obj)
mock_target_version.assert_called_with()
self.assertFalse(mock_convert.called)
@mock.patch.object(base.IronicObject, 'get_target_version', autospec=True)
@mock.patch.object(base.IronicObject, 'get_target_version')
def test_do_version_changes_for_db_downgrade(self, mock_target_version):
# obj is 1.5; convert to 1.4
mock_target_version.return_value = '1.4'
@ -471,7 +471,7 @@ class _TestObject(object):
self.assertEqual({'foo': 123, 'bar': 'test', 'missing': '',
'version': '1.4'}, changes)
self.assertEqual('1.4', obj.VERSION)
mock_target_version.assert_called_with(obj)
mock_target_version.assert_called_with()
@mock.patch('ironic.common.release_mappings.RELEASE_MAPPING',
autospec=True)
@ -610,6 +610,13 @@ class _TestObject(object):
self.assertRaises(object_exception.IncompatibleObjectVersion,
obj.get_target_version)
@mock.patch.object(base.IronicObject, 'get_target_version')
def test_supports_version(self, mock_target_version):
mock_target_version.return_value = "1.5"
obj = MyObj(self.context)
self.assertTrue(obj.supports_version((1, 5)))
self.assertFalse(obj.supports_version((1, 6)))
def test_obj_fields(self):
@base.IronicObjectRegistry.register_if(False)
class TestObj(base.IronicObject,
@ -696,7 +703,7 @@ expected_object_fingerprints = {
'NodeCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'NodeCRUDPayload': '1.2-b7a265a5e2fe47adada3c7c20c68e465',
'PortCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'PortCRUDPayload': '1.1-1ecf2d63b68014c52cb52d0227f8b5b8',
'PortCRUDPayload': '1.2-233d259df442eb15cc584fae1fe81504',
'NodeMaintenanceNotification': '1.0-59acc533c11d306f149846f922739c15',
'NodeConsoleNotification': '1.0-59acc533c11d306f149846f922739c15',
'PortgroupCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
@ -819,7 +826,7 @@ class TestObjectSerializer(test_base.TestCase):
self.assertFalse(mock_release_mapping.called)
@mock.patch.object(base.IronicObject, 'convert_to_version', autospec=True)
@mock.patch.object(base.IronicObject, 'get_target_version', autospec=True)
@mock.patch.object(base.IronicObject, 'get_target_version')
def test_serialize_entity_unpinned_api(self, mock_version, mock_convert):
"""Test single element serializer with no backport, unpinned."""
mock_version.return_value = MyObj.VERSION
@ -840,7 +847,7 @@ class TestObjectSerializer(test_base.TestCase):
self.assertFalse(mock_convert.called)
@mock.patch.object(base.IronicObject, 'convert_to_version', autospec=True)
@mock.patch.object(base.IronicObject, 'get_target_version', autospec=True)
@mock.patch.object(base.IronicObject, 'get_target_version')
def test_serialize_entity_unpinned_conductor(self, mock_version,
mock_convert):
"""Test single element serializer with no backport, unpinned."""
@ -858,10 +865,10 @@ class TestObjectSerializer(test_base.TestCase):
self.assertEqual('textt', data['missing'])
changes = primitive['ironic_object.changes']
self.assertEqual(set(['foo', 'bar', 'missing']), set(changes))
mock_version.assert_called_once_with(mock.ANY)
mock_version.assert_called_once_with()
self.assertFalse(mock_convert.called)
@mock.patch.object(base.IronicObject, 'get_target_version', autospec=True)
@mock.patch.object(base.IronicObject, 'get_target_version')
def test_serialize_entity_pinned_api(self, mock_version):
"""Test single element serializer with backport to pinned version."""
mock_version.return_value = '1.4'
@ -880,7 +887,7 @@ class TestObjectSerializer(test_base.TestCase):
self.assertEqual('miss', data['missing'])
self.assertFalse(mock_version.called)
@mock.patch.object(base.IronicObject, 'get_target_version', autospec=True)
@mock.patch.object(base.IronicObject, 'get_target_version')
def test_serialize_entity_pinned_conductor(self, mock_version):
"""Test single element serializer with backport to pinned version."""
mock_version.return_value = '1.4'
@ -898,9 +905,9 @@ class TestObjectSerializer(test_base.TestCase):
self.assertEqual('text', data['bar'])
self.assertNotIn('missing', data)
self.assertNotIn('ironic_object.changes', primitive)
mock_version.assert_called_once_with(mock.ANY)
mock_version.assert_called_once_with()
@mock.patch.object(base.IronicObject, 'get_target_version', autospec=True)
@mock.patch.object(base.IronicObject, 'get_target_version')
def test_serialize_entity_invalid_pin(self, mock_version):
mock_version.side_effect = object_exception.InvalidTargetVersion(
version='1.6')
@ -909,7 +916,7 @@ class TestObjectSerializer(test_base.TestCase):
obj = MyObj(self.context)
self.assertRaises(object_exception.InvalidTargetVersion,
serializer.serialize_entity, self.context, obj)
mock_version.assert_called_once_with(mock.ANY)
mock_version.assert_called_once_with()
@mock.patch.object(base.IronicObject, 'convert_to_version', autospec=True)
def _test__process_object(self, mock_convert, is_server=True):

View File

@ -16,14 +16,18 @@
import datetime
import mock
from oslo_config import cfg
from testtools import matchers
from ironic.common import exception
from ironic import objects
from ironic.objects import base as obj_base
from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.db import utils as db_utils
from ironic.tests.unit.objects import utils as obj_utils
CONF = cfg.CONF
class TestPortObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn):
@ -142,5 +146,17 @@ class TestPortObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn):
self.assertIsInstance(ports[0], objects.Port)
self.assertEqual(self.context, ports[0]._context)
@mock.patch.object(obj_base.IronicObject, 'supports_version')
def test_supports_physical_network_supported(self, mock_sv):
mock_sv.return_value = True
self.assertTrue(objects.Port.supports_physical_network())
mock_sv.assert_called_once_with((1, 7))
@mock.patch.object(obj_base.IronicObject, 'supports_version')
def test_supports_physical_network_unsupported(self, mock_sv):
mock_sv.return_value = False
self.assertFalse(objects.Port.supports_physical_network())
mock_sv.assert_called_once_with((1, 7))
def test_payload_schemas(self):
self._check_payload_schemas(objects.port, objects.Port.fields)

View File

@ -0,0 +1,34 @@
---
features:
- |
Adds a ``physical_network`` field to the port object in REST API version
1.34.
This field specifies the name of the physical network to which the port is
connected, and is empty by default. This field may be set by the operator
to allow the Bare Metal service to incorporate physical network information
when attaching virtual interfaces (VIFs).
The REST API endpoints related to ports provide support for the
``physical_network`` field. The `ironic developer documentation
<https://docs.openstack.org/ironic/latest/admin/multitenancy.html>`_
provides information on how to configure and use physical networks.
upgrade:
- |
Adds a ``physical_network`` field to the port object in REST API version
1.34.
This field specifies the name of the physical network to which the port is
connected, and is empty by default. This field may be set by the operator
to allow the Bare Metal service to incorporate physical network information
when attaching virtual interfaces (VIFs).
The REST API endpoints related to ports provide support for the
``physical_network`` field. The `ironic developer documentation
<https://docs.openstack.org/ironic/latest/admin/multitenancy.html>`_
provides information on how to configure and use physical networks.
Following an upgrade to this release, all ports will have an empty
``physical_network`` field. Attachment of Virtual Interfaces (VIFs) will
continue to function as in the previous release until any ports have their
physical network field set.