From 02fff930fbab4caaa1181da9ee0f70523fe8d7ca Mon Sep 17 00:00:00 2001 From: Mark Goddard Date: Thu, 1 Jun 2017 15:36:58 +0100 Subject: [PATCH] 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 --- api-ref/regenerate-samples.sh | 2 +- .../source/baremetal-api-v1-nodes-ports.inc | 5 + .../baremetal-api-v1-portgroups-ports.inc | 5 + api-ref/source/baremetal-api-v1-ports.inc | 15 + api-ref/source/parameters.yaml | 7 + .../samples/node-port-detail-response.json | 1 + .../source/samples/port-create-request.json | 3 +- .../source/samples/port-create-response.json | 1 + .../samples/port-list-detail-response.json | 1 + .../source/samples/port-update-response.json | 1 + .../portgroup-port-detail-response.json | 1 + doc/source/admin/multitenancy.rst | 101 +++-- doc/source/admin/notifications.rst | 3 +- doc/source/admin/portgroups.rst | 13 + .../contributor/webapi-version-history.rst | 5 + ironic/api/controllers/v1/port.py | 17 +- ironic/api/controllers/v1/utils.py | 13 + ironic/api/controllers/v1/versions.py | 4 +- ironic/objects/base.py | 35 +- ironic/objects/port.py | 14 +- ironic/tests/unit/api/utils.py | 2 - ironic/tests/unit/api/v1/test_ports.py | 345 +++++++++++++++++- ironic/tests/unit/api/v1/test_utils.py | 18 + ironic/tests/unit/objects/test_objects.py | 33 +- ironic/tests/unit/objects/test_port.py | 16 + ...ort-physical-network-a7009dc514353796.yaml | 34 ++ 26 files changed, 627 insertions(+), 68 deletions(-) create mode 100644 releasenotes/notes/port-physical-network-a7009dc514353796.yaml diff --git a/api-ref/regenerate-samples.sh b/api-ref/regenerate-samples.sh index 7f1328216b..4028709851 100755 --- a/api-ref/regenerate-samples.sh +++ b/api-ref/regenerate-samples.sh @@ -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 diff --git a/api-ref/source/baremetal-api-v1-nodes-ports.inc b/api-ref/source/baremetal-api-v1-nodes-ports.inc index 58addd24c1..a83cb7ab5a 100644 --- a/api-ref/source/baremetal-api-v1-nodes-ports.inc +++ b/api-ref/source/baremetal-api-v1-nodes-ports.inc @@ -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 diff --git a/api-ref/source/baremetal-api-v1-portgroups-ports.inc b/api-ref/source/baremetal-api-v1-portgroups-ports.inc index 8f0fd224dd..b0e7583fbc 100644 --- a/api-ref/source/baremetal-api-v1-portgroups-ports.inc +++ b/api-ref/source/baremetal-api-v1-portgroups-ports.inc @@ -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 diff --git a/api-ref/source/baremetal-api-v1-ports.inc b/api-ref/source/baremetal-api-v1-ports.inc index 89af7143f3..074fc242ba 100644 --- a/api-ref/source/baremetal-api-v1-ports.inc +++ b/api-ref/source/baremetal-api-v1-ports.inc @@ -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 diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 89beda4a2b..5c64b20ccd 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -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 diff --git a/api-ref/source/samples/node-port-detail-response.json b/api-ref/source/samples/node-port-detail-response.json index 1e5ea3405b..e0f4292bd5 100644 --- a/api-ref/source/samples/node-port-detail-response.json +++ b/api-ref/source/samples/node-port-detail-response.json @@ -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", diff --git a/api-ref/source/samples/port-create-request.json b/api-ref/source/samples/port-create-request.json index ef716a938a..8018f8dec7 100644 --- a/api-ref/source/samples/port-create-request.json +++ b/api-ref/source/samples/port-create-request.json @@ -6,5 +6,6 @@ "switch_id": "0a:1b:2c:3d:4e:5f", "port_id": "Ethernet3/1", "switch_info": "switch1" - } + }, + "physical_network": "physnet1" } diff --git a/api-ref/source/samples/port-create-response.json b/api-ref/source/samples/port-create-response.json index f527ec79e6..ef88e965f1 100644 --- a/api-ref/source/samples/port-create-response.json +++ b/api-ref/source/samples/port-create-response.json @@ -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, diff --git a/api-ref/source/samples/port-list-detail-response.json b/api-ref/source/samples/port-list-detail-response.json index 08c421d504..aa99ef7436 100644 --- a/api-ref/source/samples/port-list-detail-response.json +++ b/api-ref/source/samples/port-list-detail-response.json @@ -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, diff --git a/api-ref/source/samples/port-update-response.json b/api-ref/source/samples/port-update-response.json index 49d9e0517f..e0a1e07444 100644 --- a/api-ref/source/samples/port-update-response.json +++ b/api-ref/source/samples/port-update-response.json @@ -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", diff --git a/api-ref/source/samples/portgroup-port-detail-response.json b/api-ref/source/samples/portgroup-port-detail-response.json index 1e5ea3405b..e0f4292bd5 100644 --- a/api-ref/source/samples/portgroup-port-detail-response.json +++ b/api-ref/source/samples/portgroup-port-detail-response.json @@ -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", diff --git a/doc/source/admin/multitenancy.rst b/doc/source/admin/multitenancy.rst index 9b7a92025f..a9afa8baac 100644 --- a/doc/source/admin/multitenancy.rst +++ b/doc/source/admin/multitenancy.rst @@ -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= 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= #. 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: diff --git a/doc/source/admin/notifications.rst b/doc/source/admin/notifications.rst index f836185029..c547b268ef 100644 --- a/doc/source/admin/notifications.rst +++ b/doc/source/admin/notifications.rst @@ -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", diff --git a/doc/source/admin/portgroups.rst b/doc/source/admin/portgroups.rst index 179bec13a7..dde58f9aad 100644 --- a/doc/source/admin/portgroups.rst +++ b/doc/source/admin/portgroups.rst @@ -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 ` for further information on +using physical networks in the Bare Metal service. + Port groups configuration in the Bare Metal service --------------------------------------------------- diff --git a/doc/source/contributor/webapi-version-history.rst b/doc/source/contributor/webapi-version-history.rst index e750988a3f..3d7953128e 100644 --- a/doc/source/contributor/webapi-version-history.rst +++ b/doc/source/contributor/webapi-version-history.rst @@ -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 diff --git a/ironic/api/controllers/v1/port.py b/ironic/api/controllers/v1/port.py index 1c1be3b68d..a0a8cc9d21 100644 --- a/ironic/api/controllers/v1/port.py +++ b/ironic/api/controllers/v1/port.py @@ -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)): diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py index 7bc386c195..8f90d26475 100644 --- a/ironic/api/controllers/v1/utils.py +++ b/ironic/api/controllers/v1/utils.py @@ -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. diff --git a/ironic/api/controllers/v1/versions.py b/ironic/api/controllers/v1/versions.py index 7de4b6820c..a55d0727e4 100644 --- a/ironic/api/controllers/v1/versions.py +++ b/ironic/api/controllers/v1/versions.py @@ -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) diff --git a/ironic/objects/base.py b/ironic/objects/base.py index 78dfedde98..a66e685388 100644 --- a/ironic/objects/base.py +++ b/ironic/objects/base.py @@ -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. diff --git a/ironic/objects/port.py b/ironic/objects/port.py index 16c577bacf..1bfbb744d5 100644 --- a/ironic/objects/port.py +++ b/ironic/objects/port.py @@ -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() diff --git a/ironic/tests/unit/api/utils.py b/ironic/tests/unit/api/utils.py index d26864c607..cf497bc27d 100644 --- a/ironic/tests/unit/api/utils.py +++ b/ironic/tests/unit/api/utils.py @@ -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) diff --git a/ironic/tests/unit/api/v1/test_ports.py b/ironic/tests/unit/api/v1/test_ports.py index edbc8d3e95..ed63f41be3 100644 --- a/ironic/tests/unit/api/v1/test_ports.py +++ b/ironic/tests/unit/api/v1/test_ports.py @@ -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): diff --git a/ironic/tests/unit/api/v1/test_utils.py b/ironic/tests/unit/api/v1/test_utils.py index ae551fd03b..4ffb02556c 100644 --- a/ironic/tests/unit/api/v1/test_utils.py +++ b/ironic/tests/unit/api/v1/test_utils.py @@ -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): diff --git a/ironic/tests/unit/objects/test_objects.py b/ironic/tests/unit/objects/test_objects.py index b4892b0163..eb09158917 100644 --- a/ironic/tests/unit/objects/test_objects.py +++ b/ironic/tests/unit/objects/test_objects.py @@ -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): diff --git a/ironic/tests/unit/objects/test_port.py b/ironic/tests/unit/objects/test_port.py index 75d9b93293..1511cf08b6 100644 --- a/ironic/tests/unit/objects/test_port.py +++ b/ironic/tests/unit/objects/test_port.py @@ -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) diff --git a/releasenotes/notes/port-physical-network-a7009dc514353796.yaml b/releasenotes/notes/port-physical-network-a7009dc514353796.yaml new file mode 100644 index 0000000000..97c36cac0d --- /dev/null +++ b/releasenotes/notes/port-physical-network-a7009dc514353796.yaml @@ -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 + `_ + 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 + `_ + 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.