diff --git a/api-ref/source/baremetal-api-v1-nodes.inc b/api-ref/source/baremetal-api-v1-nodes.inc index 4734c1b734..47bcceb58c 100644 --- a/api-ref/source/baremetal-api-v1-nodes.inc +++ b/api-ref/source/baremetal-api-v1-nodes.inc @@ -286,7 +286,7 @@ provision state, and maintenance setting for each Node. Introduced the ``lessee`` field. .. versionadded:: 1.82 - Introduced the ``shard`` field. + Introduced the ``shard`` field. Introduced the ``sharded`` request parameter. Normal response codes: 200 @@ -309,6 +309,7 @@ Request - owner: owner - lessee: lessee - shard: req_shard + - sharded: req_sharded - description_contains: r_description_contains - fields: fields - limit: limit @@ -381,7 +382,7 @@ Nova instance, eg. with a request to ``v1/nodes/detail?instance_uuid={NOVA INSTA Introduced the ``lessee`` field. .. versionadded:: 1.82 - Introduced the ``shard`` field. + Introduced the ``shard`` field. Introduced the ``sharded`` request parameter. Normal response codes: 200 @@ -404,6 +405,7 @@ Request - owner: owner - lessee: lessee - shard: req_shard + - sharded: req_sharded - description_contains: r_description_contains - limit: limit - marker: marker diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 13eb45d7d0..6a50c94523 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -1827,6 +1827,13 @@ req_shard: in: body required: false type: array +req_sharded: + description: | + When true, filter the list of returned Nodes, and only return the ones with + a non-null ``shard`` value. When false, the inverse filter is performed. + in: body + required: false + type: boolean req_standalone_ports_supported: description: | Indicates whether ports that are members of this portgroup can be diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py index 9a117a95fd..1aa9cf9ca6 100644 --- a/ironic/api/controllers/v1/node.py +++ b/ironic/api/controllers/v1/node.py @@ -2071,7 +2071,8 @@ class NodesController(rest.RestController): fields=None, fault=None, conductor_group=None, detail=None, conductor=None, owner=None, lessee=None, project=None, - description_contains=None, shard=None): + description_contains=None, shard=None, + sharded=None): if self.from_chassis and not chassis_uuid: raise exception.MissingParameterValue( _("Chassis id not specified.")) @@ -2112,7 +2113,8 @@ class NodesController(rest.RestController): 'project': project, 'description_contains': description_contains, 'retired': retired, - 'instance_uuid': instance_uuid + 'instance_uuid': instance_uuid, + 'sharded': sharded } filters = {} for key, value in possible_filters.items(): @@ -2257,14 +2259,14 @@ class NodesController(rest.RestController): detail=args.boolean, conductor=args.string, owner=args.string, description_contains=args.string, lessee=args.string, project=args.string, - shard=args.string_list) + shard=args.string_list, sharded=args.boolean) def get_all(self, chassis_uuid=None, instance_uuid=None, associated=None, maintenance=None, retired=None, provision_state=None, marker=None, limit=None, sort_key='id', sort_dir='asc', driver=None, fields=None, resource_class=None, fault=None, conductor_group=None, detail=None, conductor=None, owner=None, description_contains=None, lessee=None, - project=None, shard=None): + project=None, shard=None, sharded=None): """Retrieve a list of nodes. :param chassis_uuid: Optional UUID of a chassis, to get only nodes for @@ -2310,6 +2312,9 @@ class NodesController(rest.RestController): :param description_contains: Optional string value to get only nodes with description field contains matching value. + :param sharded: Optional boolean whether to return a list of + nodes with or without a shard set. May be combined + with other parameters. """ project = api_utils.check_list_policy('node', project) @@ -2325,6 +2330,8 @@ class NodesController(rest.RestController): api_utils.check_allow_filter_by_owner(owner) api_utils.check_allow_filter_by_lessee(lessee) api_utils.check_allow_filter_by_shard(shard) + # Sharded is guarded by the same API version as shard + api_utils.check_allow_filter_by_shard(sharded) fields = api_utils.get_request_return_fields(fields, detail, _DEFAULT_RETURN_FIELDS) @@ -2341,8 +2348,8 @@ class NodesController(rest.RestController): detail=detail, conductor=conductor, owner=owner, lessee=lessee, - shard=shard, project=project, - **extra_args) + shard=shard, sharded=sharded, + project=project, **extra_args) @METRICS.timer('NodesController.detail') @method.expose() @@ -2355,14 +2362,14 @@ class NodesController(rest.RestController): conductor_group=args.string, conductor=args.string, owner=args.string, description_contains=args.string, lessee=args.string, project=args.string, - shard=args.string_list) + shard=args.string_list, sharded=args.boolean) def detail(self, chassis_uuid=None, instance_uuid=None, associated=None, maintenance=None, retired=None, provision_state=None, marker=None, limit=None, sort_key='id', sort_dir='asc', driver=None, resource_class=None, fault=None, conductor_group=None, conductor=None, owner=None, description_contains=None, lessee=None, project=None, - shard=None): + shard=None, sharded=None): """Retrieve a list of nodes with detail. :param chassis_uuid: Optional UUID of a chassis, to get only nodes for @@ -2403,6 +2410,9 @@ class NodesController(rest.RestController): :param description_contains: Optional string value to get only nodes with description field contains matching value. + :param sharded: Optional boolean whether to return a list of + nodes with or without a shard set. May be combined + with other parameters. """ project = api_utils.check_list_policy('node', project) @@ -2421,6 +2431,8 @@ class NodesController(rest.RestController): api_utils.check_allow_filter_by_conductor(conductor) api_utils.check_allow_filter_by_shard(shard) + # Sharded is guarded by the same API version as shard + api_utils.check_allow_filter_by_shard(sharded) extra_args = {'description_contains': description_contains} return self._get_nodes_collection(chassis_uuid, instance_uuid, @@ -2435,7 +2447,7 @@ class NodesController(rest.RestController): conductor=conductor, owner=owner, lessee=lessee, project=project, shard=shard, - **extra_args) + sharded=sharded, **extra_args) @METRICS.timer('NodesController.validate') @method.expose() diff --git a/ironic/db/sqlalchemy/api.py b/ironic/db/sqlalchemy/api.py index 474a49ec51..de39343181 100644 --- a/ironic/db/sqlalchemy/api.py +++ b/ironic/db/sqlalchemy/api.py @@ -401,7 +401,8 @@ class Connection(api.Connection): for field in ('uuid', 'provision_state', 'shard')} _NODE_NON_NULL_FILTERS = {'associated': 'instance_uuid', 'reserved': 'reservation', - 'with_power_state': 'power_state'} + 'with_power_state': 'power_state', + 'sharded': 'shard'} _NODE_FILTERS = ({'chassis_uuid', 'reserved_by_any_of', 'provisioned_before', 'inspection_started_before', 'description_contains', 'project'} diff --git a/ironic/tests/unit/api/controllers/v1/test_node.py b/ironic/tests/unit/api/controllers/v1/test_node.py index e1ef916b9a..1c4bf82d3f 100644 --- a/ironic/tests/unit/api/controllers/v1/test_node.py +++ b/ironic/tests/unit/api/controllers/v1/test_node.py @@ -8028,6 +8028,17 @@ class TestNodeShardGets(test_api_base.BaseApiTest): expect_errors=True, headers=headers) self.assertEqual(http_client.NOT_ACCEPTABLE, result.status_code) + def test_filtering_by_sharded(self): + obj_utils.create_test_node(self.context, uuid=uuid.uuid4()) + obj_utils.create_test_node(self.context, uuid=uuid.uuid4()) + # We now have one node in shard foo (setUp) and two unsharded. + result_true = self.get_json( + '/nodes?sharded=true', headers=self.headers) + result_false = self.get_json( + '/nodes?sharded=false', headers=self.headers) + self.assertEqual(1, len(result_true['nodes'])) + self.assertEqual(2, len(result_false['nodes'])) + @mock.patch.object(rpcapi.ConductorAPI, 'create_node', lambda _api, _ctx, node, _topic: _create_node_locally(node))