diff --git a/doc/source/webapi/v1.rst b/doc/source/webapi/v1.rst index 6a3d13c6c9..c0e2d6ae58 100644 --- a/doc/source/webapi/v1.rst +++ b/doc/source/webapi/v1.rst @@ -32,6 +32,9 @@ always requests the newest supported API version. API Versions History -------------------- +**1.16** + Add ability to filter nodes by driver. + **1.15** Add ability to do manual cleaning when a node is in the manageable diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py index a2c29be195..324f55cb7f 100644 --- a/ironic/api/controllers/v1/node.py +++ b/ironic/api/controllers/v1/node.py @@ -974,8 +974,8 @@ class NodesController(rest.RestController): def _get_nodes_collection(self, chassis_uuid, instance_uuid, associated, maintenance, provision_state, marker, limit, - sort_key, sort_dir, resource_url=None, - fields=None): + sort_key, sort_dir, driver=None, + resource_url=None, fields=None): if self.from_chassis and not chassis_uuid: raise exception.MissingParameterValue( _("Chassis id not specified.")) @@ -1005,6 +1005,8 @@ class NodesController(rest.RestController): filters['maintenance'] = maintenance if provision_state: filters['provision_state'] = provision_state + if driver: + filters['driver'] = driver nodes = objects.Node.list(pecan.request.context, limit, marker_obj, sort_key=sort_key, sort_dir=sort_dir, @@ -1082,14 +1084,15 @@ class NodesController(rest.RestController): @expose.expose(NodeCollection, types.uuid, types.uuid, types.boolean, types.boolean, wtypes.text, types.uuid, int, wtypes.text, - wtypes.text, types.listtype) + wtypes.text, wtypes.text, types.listtype) def get_all(self, chassis_uuid=None, instance_uuid=None, associated=None, maintenance=None, provision_state=None, marker=None, - limit=None, sort_key='id', sort_dir='asc', fields=None): + limit=None, sort_key='id', sort_dir='asc', driver=None, + fields=None): """Retrieve a list of nodes. :param chassis_uuid: Optional UUID of a chassis, to get only nodes for - that chassis. + that chassis. :param instance_uuid: Optional UUID of an instance, to find the node associated with that instance. :param associated: Optional boolean whether to return a list of @@ -1104,25 +1107,28 @@ class NodesController(rest.RestController): :param limit: maximum number of resources to return in a single result. :param sort_key: column to sort results by. Default: id. :param sort_dir: direction to sort. "asc" or "desc". Default: asc. + :param driver: Optional string value to get only nodes using that + driver. :param fields: Optional, a list with a specified set of fields - of the resource to be returned. + of the resource to be returned. """ api_utils.check_allow_specify_fields(fields) api_utils.check_for_invalid_state_and_allow_filter(provision_state) + api_utils.check_allow_specify_driver(driver) if fields is None: fields = _DEFAULT_RETURN_FIELDS return self._get_nodes_collection(chassis_uuid, instance_uuid, associated, maintenance, provision_state, marker, limit, sort_key, sort_dir, - fields=fields) + driver, fields=fields) @expose.expose(NodeCollection, types.uuid, types.uuid, types.boolean, types.boolean, wtypes.text, types.uuid, int, wtypes.text, - wtypes.text) + wtypes.text, wtypes.text) def detail(self, chassis_uuid=None, instance_uuid=None, associated=None, maintenance=None, provision_state=None, marker=None, - limit=None, sort_key='id', sort_dir='asc'): + limit=None, sort_key='id', sort_dir='asc', driver=None): """Retrieve a list of nodes with detail. :param chassis_uuid: Optional UUID of a chassis, to get only nodes for @@ -1141,8 +1147,11 @@ class NodesController(rest.RestController): :param limit: maximum number of resources to return in a single result. :param sort_key: column to sort results by. Default: id. :param sort_dir: direction to sort. "asc" or "desc". Default: asc. + :param driver: Optional string value to get only nodes using that + driver. """ api_utils.check_for_invalid_state_and_allow_filter(provision_state) + api_utils.check_allow_specify_driver(driver) # /detail should only work against collections parent = pecan.request.path.split('/')[:-1][-1] if parent != "nodes": @@ -1153,7 +1162,7 @@ class NodesController(rest.RestController): associated, maintenance, provision_state, marker, limit, sort_key, sort_dir, - resource_url) + driver, resource_url) @expose.expose(wtypes.text, types.uuid_or_name, types.uuid) def validate(self, node=None, node_uuid=None): diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py index b88fe65219..e117f590eb 100644 --- a/ironic/api/controllers/v1/utils.py +++ b/ironic/api/controllers/v1/utils.py @@ -214,6 +214,20 @@ def check_for_invalid_state_and_allow_filter(provision_state): _('Provision state "%s" is not valid') % provision_state) +def check_allow_specify_driver(driver): + """Check if filtering nodes by driver is allowed. + + Version 1.16 of the API allows filter nodes by driver. + """ + if (driver is not None and pecan.request.version.minor < + versions.MINOR_16_DRIVER_FILTER): + raise exception.NotAcceptable(_( + "Request not acceptable. The minimal required API version " + "should be %(base)s.%(opr)s") % + {'base': versions.BASE_VERSION, + 'opr': versions.MINOR_16_DRIVER_FILTER}) + + def initial_node_provision_state(): """Return node state to use by default when creating new nodes. diff --git a/ironic/api/controllers/v1/versions.py b/ironic/api/controllers/v1/versions.py index 82545d042f..0acdf67016 100644 --- a/ironic/api/controllers/v1/versions.py +++ b/ironic/api/controllers/v1/versions.py @@ -45,6 +45,7 @@ BASE_VERSION = 1 # 1. '/v1/nodes//states' # 2. '/v1/drivers//properties' # v1.15: Add ability to do manual cleaning of nodes +# v1.16: Add ability to filter nodes by driver. MINOR_0_JUNO = 0 MINOR_1_INITIAL_VERSION = 1 @@ -62,11 +63,12 @@ MINOR_12_RAID_CONFIG = 12 MINOR_13_ABORT_VERB = 13 MINOR_14_LINKS_NODESTATES_DRIVERPROPERTIES = 14 MINOR_15_MANUAL_CLEAN = 15 +MINOR_16_DRIVER_FILTER = 16 # When adding another version, update MINOR_MAX_VERSION and also update # doc/source/webapi/v1.rst with a detailed explanation of what the version has # changed. -MINOR_MAX_VERSION = MINOR_15_MANUAL_CLEAN +MINOR_MAX_VERSION = MINOR_16_DRIVER_FILTER # String representations of the minor and maximum versions MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION) diff --git a/ironic/tests/unit/api/v1/test_nodes.py b/ironic/tests/unit/api/v1/test_nodes.py index 70246d0cf3..3b5a9283c0 100644 --- a/ironic/tests/unit/api/v1/test_nodes.py +++ b/ironic/tests/unit/api/v1/test_nodes.py @@ -695,6 +695,38 @@ class TestListNodes(test_api_base.BaseApiTest): self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code) self.assertTrue(response.json['error_message']) + def test_get_nodes_by_driver(self): + node = obj_utils.create_test_node(self.context, + uuid=uuidutils.generate_uuid(), + driver='pxe_ssh') + node1 = obj_utils.create_test_node(self.context, + uuid=uuidutils.generate_uuid(), + driver='fake') + + data = self.get_json('/nodes?driver=pxe_ssh', + headers={api_base.Version.string: "1.16"}) + uuids = [n['uuid'] for n in data['nodes']] + self.assertIn(node.uuid, uuids) + self.assertNotIn(node1.uuid, uuids) + data = self.get_json('/nodes?driver=fake', + headers={api_base.Version.string: "1.16"}) + uuids = [n['uuid'] for n in data['nodes']] + self.assertIn(node1.uuid, uuids) + self.assertNotIn(node.uuid, uuids) + + def test_get_nodes_by_invalid_driver(self): + data = self.get_json('/nodes?driver=test', + headers={api_base.Version.string: "1.16"}) + self.assertEqual(0, len(data['nodes'])) + + def test_get_nodes_by_driver_invalid_api_version(self): + response = self.get_json( + '/nodes?driver=fake', + headers={api_base.Version.string: str(api_v1.MIN_VER)}, + expect_errors=True) + self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code) + self.assertTrue(response.json['error_message']) + def test_get_console_information(self): node = obj_utils.create_test_node(self.context) expected_console_info = {'test': 'test-data'} diff --git a/ironic/tests/unit/api/v1/test_utils.py b/ironic/tests/unit/api/v1/test_utils.py index 45bb6609de..97381ff05f 100644 --- a/ironic/tests/unit/api/v1/test_utils.py +++ b/ironic/tests/unit/api/v1/test_utils.py @@ -97,6 +97,17 @@ class TestApiUtils(base.TestCase): self.assertRaises(exception.NotAcceptable, utils.check_allow_specify_fields, ['foo']) + @mock.patch.object(pecan, 'request', spec_set=['version']) + def test_check_allow_specify_driver(self, mock_request): + mock_request.version.minor = 16 + self.assertIsNone(utils.check_allow_specify_driver(['fake'])) + + @mock.patch.object(pecan, 'request', spec_set=['version']) + def test_check_allow_specify_driver_fail(self, mock_request): + mock_request.version.minor = 15 + self.assertRaises(exception.NotAcceptable, + utils.check_allow_specify_driver, ['fake']) + @mock.patch.object(pecan, 'request', spec_set=['version']) def test_allow_links_node_states_and_driver_properties(self, mock_request): mock_request.version.minor = 14 diff --git a/releasenotes/notes/list-nodes-by-driver-a1ab9f2b73f652f8.yaml b/releasenotes/notes/list-nodes-by-driver-a1ab9f2b73f652f8.yaml new file mode 100644 index 0000000000..e5c5c1a774 --- /dev/null +++ b/releasenotes/notes/list-nodes-by-driver-a1ab9f2b73f652f8.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add support for filtering nodes using the same driver via the API.