From ee06761b0ec5edb42199c1b96ddcd4900d50002b Mon Sep 17 00:00:00 2001 From: Tadeas Kot Date: Thu, 12 Aug 2021 17:53:16 +0200 Subject: [PATCH] Add support for fields in drivers API This commit add support for the fields query parameter to the GET /v1/drivers?fields=... and GET /v1/drivers/?fields=.... Story: 1674775 Task: 10581 Change-Id: I2ca4eb490e320e736a93851eed526ec862be901e --- api-ref/source/baremetal-api-v1-drivers.inc | 8 +++ .../contributor/webapi-version-history.rst | 7 ++ ironic/api/controllers/v1/driver.py | 50 ++++++++++--- ironic/api/controllers/v1/versions.py | 4 +- ironic/common/release_mappings.py | 2 +- .../unit/api/controllers/v1/test_driver.py | 71 +++++++++++++++++++ ...-api-fields-selector-36f12259f01b0f7a.yaml | 9 +++ 7 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/add-driver-api-fields-selector-36f12259f01b0f7a.yaml diff --git a/api-ref/source/baremetal-api-v1-drivers.inc b/api-ref/source/baremetal-api-v1-drivers.inc index b7d8518140..c680aa0ec3 100644 --- a/api-ref/source/baremetal-api-v1-drivers.inc +++ b/api-ref/source/baremetal-api-v1-drivers.inc @@ -49,6 +49,9 @@ List drivers Lists all drivers. +.. versionadded:: 1.77 + Added ``fields`` selector to query for particular fields. + Normal response codes: 200 Request @@ -58,6 +61,7 @@ Request - type: driver_type - detail: driver_detail + - fields: fields Response Parameters ------------------- @@ -125,6 +129,9 @@ Show driver details Shows details for a driver. +.. versionadded:: 1.77 + Added ``fields`` selector to query for particular fields. + Normal response codes: 200 Request @@ -133,6 +140,7 @@ Request .. rest_parameters:: parameters.yaml - driver_name: driver_ident + - fields: fields Response Parameters ------------------- diff --git a/doc/source/contributor/webapi-version-history.rst b/doc/source/contributor/webapi-version-history.rst index 6d6b715c78..c83cf2698c 100644 --- a/doc/source/contributor/webapi-version-history.rst +++ b/doc/source/contributor/webapi-version-history.rst @@ -2,6 +2,13 @@ REST API Version History ======================== +1.77 +---------------------- +Add a fields selector to the the Drivers list: +* ``GET /v1/drivers?fields=`` +Also add a fields selector to the the Driver detail: +* ``GET /v1/drivers/{driver_name}?fields=`` + 1.76 (Xena, ?) ---------------------- Add endpoints for changing boot mode and secure boot state of node diff --git a/ironic/api/controllers/v1/driver.py b/ironic/api/controllers/v1/driver.py index 9027e4638d..2775ed284b 100644 --- a/ironic/api/controllers/v1/driver.py +++ b/ironic/api/controllers/v1/driver.py @@ -81,7 +81,8 @@ def hide_fields_in_newer_versions(driver): driver.pop('enabled_bios_interfaces', None) -def convert_with_links(name, hosts, detail=False, interface_info=None): +def convert_with_links(name, hosts, detail=False, interface_info=None, + fields=None, sanitize=True): """Convert driver/hardware type info to a dict. :param name: name of a hardware type. @@ -90,6 +91,8 @@ def convert_with_links(name, hosts, detail=False, interface_info=None): the 'type' field and default/enabled interfaces fields. :param interface_info: optional list of dicts of hardware interface info. + :param fields: list of fields to preserve, or ``None`` to preserve default + :param sanitize: boolean, sanitize driver :returns: dict representing the driver object. """ driver = { @@ -143,16 +146,35 @@ def convert_with_links(name, hosts, detail=False, interface_info=None): driver[enabled_key] = list(enabled) hide_fields_in_newer_versions(driver) + + if not sanitize: + return driver + + driver_sanitize(driver, fields) + return driver -def list_convert_with_links(hardware_types, detail=False): +def driver_sanitize(driver, fields=None): + if fields is not None: + api_utils.sanitize_dict(driver, fields) + api_utils.check_for_invalid_fields(fields, driver) + + +def _check_allow_driver_fields(fields): + if (fields is not None and api.request.version.minor + < api.controllers.v1.versions.MINOR_77_DRIVER_FIELDS_SELECTOR): + raise exception.NotAcceptable() + + +def list_convert_with_links(hardware_types, detail=False, fields=None): """Convert drivers and hardware types to an API-serializable object. :param hardware_types: dict mapping hardware type names to conductor hostnames. :param detail: boolean, whether to include detailed info, such as the 'type' field and default/enabled interfaces fields. + :param fields: list of fields to preserve, or ``None`` to preserve default :returns: an API-serializable driver collection object. """ drivers = [] @@ -177,7 +199,8 @@ def list_convert_with_links(hardware_types, detail=False): convert_with_links(htname, list(hardware_types[htname]), detail=detail, - interface_info=interface_info)) + interface_info=interface_info, + fields=fields)) return collection @@ -294,16 +317,22 @@ class DriversController(rest.RestController): @METRICS.timer('DriversController.get_all') @method.expose() - @args.validate(type=args.string, detail=args.boolean) - def get_all(self, type=None, detail=None): + @args.validate(type=args.string, detail=args.boolean, + fields=args.string_list) + def get_all(self, type=None, detail=None, fields=None): """Retrieve a list of drivers.""" # FIXME(tenbrae): formatting of the auto-generated REST API docs # will break from a single-line doc string. # This is a result of a bug in sphinxcontrib-pecanwsme # https://github.com/dreamhost/sphinxcontrib-pecanwsme/issues/8 + if fields and detail: + raise exception.InvalidParameterValue( + "Can not specify ?detail=True and fields in the same request.") + api_utils.check_policy('baremetal:driver:get') api_utils.check_allow_driver_detail(detail) api_utils.check_allow_filter_driver_type(type) + _check_allow_driver_fields(fields) if type not in (None, 'classic', 'dynamic'): raise exception.Invalid(_( '"type" filter must be one of "classic" or "dynamic", ' @@ -315,12 +344,13 @@ class DriversController(rest.RestController): # NOTE(dtantsur): we don't support classic drivers starting with # the Rocky release. hw_type_dict = {} - return list_convert_with_links(hw_type_dict, detail=detail) + return list_convert_with_links(hw_type_dict, detail=detail, + fields=fields) @METRICS.timer('DriversController.get_one') @method.expose() - @args.validate(driver_name=args.string) - def get_one(self, driver_name): + @args.validate(driver_name=args.string, fields=args.string_list) + def get_one(self, driver_name, fields=None): """Retrieve a single driver.""" # NOTE(russell_h): There is no way to make this more efficient than # retrieving a list of drivers using the current sqlalchemy schema, but @@ -328,11 +358,13 @@ class DriversController(rest.RestController): # choose to expose below it. api_utils.check_policy('baremetal:driver:get') + _check_allow_driver_fields(fields) + hw_type_dict = api.request.dbapi.get_active_hardware_type_dict() for name, hosts in hw_type_dict.items(): if name == driver_name: return convert_with_links(name, list(hosts), - detail=True) + detail=True, fields=fields) raise exception.DriverNotFound(driver_name=driver_name) diff --git a/ironic/api/controllers/v1/versions.py b/ironic/api/controllers/v1/versions.py index 9cd890e8f8..0489b1dbe4 100644 --- a/ironic/api/controllers/v1/versions.py +++ b/ironic/api/controllers/v1/versions.py @@ -114,6 +114,7 @@ BASE_VERSION = 1 # v1.74: Add bios registry to /v1/nodes/{node}/bios/{setting} # v1.75: Add boot_mode, secure_boot fields to node object. # v1.76: Add support for changing boot_mode and secure_boot state +# v1.77: Add fields selector to drivers list and driver detail. MINOR_0_JUNO = 0 MINOR_1_INITIAL_VERSION = 1 @@ -192,6 +193,7 @@ MINOR_73_DEPLOY_UNDEPLOY_VERBS = 73 MINOR_74_BIOS_REGISTRY = 74 MINOR_75_NODE_BOOT_MODE = 75 MINOR_76_NODE_CHANGE_BOOT_MODE = 76 +MINOR_77_DRIVER_FIELDS_SELECTOR = 77 # When adding another version, update: # - MINOR_MAX_VERSION @@ -199,7 +201,7 @@ MINOR_76_NODE_CHANGE_BOOT_MODE = 76 # explanation of what changed in the new version # - common/release_mappings.py, RELEASE_MAPPING['master']['api'] -MINOR_MAX_VERSION = MINOR_76_NODE_CHANGE_BOOT_MODE +MINOR_MAX_VERSION = MINOR_77_DRIVER_FIELDS_SELECTOR # String representations of the minor and maximum versions _MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION) diff --git a/ironic/common/release_mappings.py b/ironic/common/release_mappings.py index ee7634aaba..6af1074e79 100644 --- a/ironic/common/release_mappings.py +++ b/ironic/common/release_mappings.py @@ -371,7 +371,7 @@ RELEASE_MAPPING = { } }, 'master': { - 'api': '1.76', + 'api': '1.77', 'rpc': '1.55', 'objects': { 'Allocation': ['1.1'], diff --git a/ironic/tests/unit/api/controllers/v1/test_driver.py b/ironic/tests/unit/api/controllers/v1/test_driver.py index b991ac7af9..6bf04297ff 100644 --- a/ironic/tests/unit/api/controllers/v1/test_driver.py +++ b/ironic/tests/unit/api/controllers/v1/test_driver.py @@ -274,6 +274,77 @@ class TestListDrivers(base.BaseApiTest): response = self.get_json('/drivers/nope', expect_errors=True) self.assertEqual(http_client.NOT_FOUND, response.status_int) + def test_drivers_collection_custom_fields(self): + self.register_fake_conductors() + fields = "name,hosts" + data = self.get_json('/drivers?fields=%s' % fields, + headers={api_base.Version.string: '1.77'}) + for data_driver in data['drivers']: + self.assertCountEqual(['name', 'hosts', 'links'], data_driver) + + def test_get_one_custom_fields(self): + self.register_fake_conductors() + driver = self.hw1 + fields = "name,hosts" + data = self.get_json('/drivers/%s?fields=%s' % (driver, fields), + headers={api_base.Version.string: '1.77'}) + self.assertCountEqual(['name', 'hosts', 'links'], data) + + def test_get_custom_fields_invalid_api_version(self): + self.register_fake_conductors() + driver = self.hw1 + fields = "name,hosts" + + response = self.get_json('/drivers?fields=%s' % fields, + headers={api_base.Version.string: '1.76'}, + expect_errors=True) + self.assertEqual('application/json', response.content_type) + self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int) + + response = self.get_json('/drivers/%s?fields=%s' % (driver, fields), + headers={api_base.Version.string: '1.76'}, + expect_errors=True) + self.assertEqual('application/json', response.content_type) + self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int) + + def test_drivers_collection_custom_fields_with_detail_true(self): + self.register_fake_conductors() + fields = "name,hosts" + response = self.get_json('/drivers?detail=true&fields=%s' % fields, + headers={api_base.Version.string: '1.77'}, + expect_errors=True) + self.assertEqual('application/json', response.content_type) + self.assertEqual(http_client.BAD_REQUEST, response.status_int) + + def test_drivers_collection_custom_fields_with_detail_false(self): + self.register_fake_conductors() + fields = "name,hosts" + data = self.get_json('/drivers?fields=%s&detail=false' % fields, + headers={api_base.Version.string: '1.77'}) + for data_driver in data['drivers']: + self.assertCountEqual(['name', 'hosts', 'links'], data_driver) + + def test_drivers_collection_invalid_custom_fields(self): + self.register_fake_conductors() + fields = "name,invalid" + response = self.get_json('/drivers?fields=%s' % fields, + headers={api_base.Version.string: '1.77'}, + expect_errors=True) + self.assertEqual(http_client.BAD_REQUEST, response.status_int) + self.assertEqual('application/json', response.content_type) + self.assertIn('invalid', response.json['error_message']) + + def test_get_one_invalid_custom_fields(self): + self.register_fake_conductors() + driver = self.hw1 + fields = "name,invalid" + response = self.get_json('/drivers/%s?fields=%s' % (driver, fields), + headers={api_base.Version.string: '1.77'}, + expect_errors=True) + self.assertEqual(http_client.BAD_REQUEST, response.status_int) + self.assertEqual('application/json', response.content_type) + self.assertIn('invalid', response.json['error_message']) + def _test_links(self, public_url=None): cfg.CONF.set_override('public_endpoint', public_url, 'api') self.register_fake_conductors() diff --git a/releasenotes/notes/add-driver-api-fields-selector-36f12259f01b0f7a.yaml b/releasenotes/notes/add-driver-api-fields-selector-36f12259f01b0f7a.yaml new file mode 100644 index 0000000000..ca5e45bc99 --- /dev/null +++ b/releasenotes/notes/add-driver-api-fields-selector-36f12259f01b0f7a.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Adds support for fields selector in driver api. + See `story 1674775 + `_. + + * ``GET /v1/drivers?fields=...`` + * ``GET /v1/drivers/{driver_name}?fields=...``