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:
parent
d9983f1eec
commit
02fff930fb
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -6,5 +6,6 @@
|
||||
"switch_id": "0a:1b:2c:3d:4e:5f",
|
||||
"port_id": "Ethernet3/1",
|
||||
"switch_info": "switch1"
|
||||
}
|
||||
},
|
||||
"physical_network": "physnet1"
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
---------------------------------------------------
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)):
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user