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/<driver_name>?fields=....

Story: 1674775
Task: 10581
Change-Id: I2ca4eb490e320e736a93851eed526ec862be901e
This commit is contained in:
Tadeas Kot 2021-08-12 17:53:16 +02:00
parent 183325d464
commit ee06761b0e
7 changed files with 140 additions and 11 deletions

View File

@ -49,6 +49,9 @@ List drivers
Lists all drivers. Lists all drivers.
.. versionadded:: 1.77
Added ``fields`` selector to query for particular fields.
Normal response codes: 200 Normal response codes: 200
Request Request
@ -58,6 +61,7 @@ Request
- type: driver_type - type: driver_type
- detail: driver_detail - detail: driver_detail
- fields: fields
Response Parameters Response Parameters
------------------- -------------------
@ -125,6 +129,9 @@ Show driver details
Shows details for a driver. Shows details for a driver.
.. versionadded:: 1.77
Added ``fields`` selector to query for particular fields.
Normal response codes: 200 Normal response codes: 200
Request Request
@ -133,6 +140,7 @@ Request
.. rest_parameters:: parameters.yaml .. rest_parameters:: parameters.yaml
- driver_name: driver_ident - driver_name: driver_ident
- fields: fields
Response Parameters Response Parameters
------------------- -------------------

View File

@ -2,6 +2,13 @@
REST API Version History 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, ?) 1.76 (Xena, ?)
---------------------- ----------------------
Add endpoints for changing boot mode and secure boot state of node Add endpoints for changing boot mode and secure boot state of node

View File

@ -81,7 +81,8 @@ def hide_fields_in_newer_versions(driver):
driver.pop('enabled_bios_interfaces', None) 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. """Convert driver/hardware type info to a dict.
:param name: name of a hardware type. :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. the 'type' field and default/enabled interfaces fields.
:param interface_info: optional list of dicts of hardware interface :param interface_info: optional list of dicts of hardware interface
info. info.
:param fields: list of fields to preserve, or ``None`` to preserve default
:param sanitize: boolean, sanitize driver
:returns: dict representing the driver object. :returns: dict representing the driver object.
""" """
driver = { driver = {
@ -143,16 +146,35 @@ def convert_with_links(name, hosts, detail=False, interface_info=None):
driver[enabled_key] = list(enabled) driver[enabled_key] = list(enabled)
hide_fields_in_newer_versions(driver) hide_fields_in_newer_versions(driver)
if not sanitize:
return driver
driver_sanitize(driver, fields)
return driver 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. """Convert drivers and hardware types to an API-serializable object.
:param hardware_types: dict mapping hardware type names to conductor :param hardware_types: dict mapping hardware type names to conductor
hostnames. hostnames.
:param detail: boolean, whether to include detailed info, such as :param detail: boolean, whether to include detailed info, such as
the 'type' field and default/enabled interfaces fields. 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. :returns: an API-serializable driver collection object.
""" """
drivers = [] drivers = []
@ -177,7 +199,8 @@ def list_convert_with_links(hardware_types, detail=False):
convert_with_links(htname, convert_with_links(htname,
list(hardware_types[htname]), list(hardware_types[htname]),
detail=detail, detail=detail,
interface_info=interface_info)) interface_info=interface_info,
fields=fields))
return collection return collection
@ -294,16 +317,22 @@ class DriversController(rest.RestController):
@METRICS.timer('DriversController.get_all') @METRICS.timer('DriversController.get_all')
@method.expose() @method.expose()
@args.validate(type=args.string, detail=args.boolean) @args.validate(type=args.string, detail=args.boolean,
def get_all(self, type=None, detail=None): fields=args.string_list)
def get_all(self, type=None, detail=None, fields=None):
"""Retrieve a list of drivers.""" """Retrieve a list of drivers."""
# FIXME(tenbrae): formatting of the auto-generated REST API docs # FIXME(tenbrae): formatting of the auto-generated REST API docs
# will break from a single-line doc string. # will break from a single-line doc string.
# This is a result of a bug in sphinxcontrib-pecanwsme # This is a result of a bug in sphinxcontrib-pecanwsme
# https://github.com/dreamhost/sphinxcontrib-pecanwsme/issues/8 # 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_policy('baremetal:driver:get')
api_utils.check_allow_driver_detail(detail) api_utils.check_allow_driver_detail(detail)
api_utils.check_allow_filter_driver_type(type) api_utils.check_allow_filter_driver_type(type)
_check_allow_driver_fields(fields)
if type not in (None, 'classic', 'dynamic'): if type not in (None, 'classic', 'dynamic'):
raise exception.Invalid(_( raise exception.Invalid(_(
'"type" filter must be one of "classic" or "dynamic", ' '"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 # NOTE(dtantsur): we don't support classic drivers starting with
# the Rocky release. # the Rocky release.
hw_type_dict = {} 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') @METRICS.timer('DriversController.get_one')
@method.expose() @method.expose()
@args.validate(driver_name=args.string) @args.validate(driver_name=args.string, fields=args.string_list)
def get_one(self, driver_name): def get_one(self, driver_name, fields=None):
"""Retrieve a single driver.""" """Retrieve a single driver."""
# NOTE(russell_h): There is no way to make this more efficient than # NOTE(russell_h): There is no way to make this more efficient than
# retrieving a list of drivers using the current sqlalchemy schema, but # retrieving a list of drivers using the current sqlalchemy schema, but
@ -328,11 +358,13 @@ class DriversController(rest.RestController):
# choose to expose below it. # choose to expose below it.
api_utils.check_policy('baremetal:driver:get') api_utils.check_policy('baremetal:driver:get')
_check_allow_driver_fields(fields)
hw_type_dict = api.request.dbapi.get_active_hardware_type_dict() hw_type_dict = api.request.dbapi.get_active_hardware_type_dict()
for name, hosts in hw_type_dict.items(): for name, hosts in hw_type_dict.items():
if name == driver_name: if name == driver_name:
return convert_with_links(name, list(hosts), return convert_with_links(name, list(hosts),
detail=True) detail=True, fields=fields)
raise exception.DriverNotFound(driver_name=driver_name) raise exception.DriverNotFound(driver_name=driver_name)

View File

@ -114,6 +114,7 @@ BASE_VERSION = 1
# v1.74: Add bios registry to /v1/nodes/{node}/bios/{setting} # v1.74: Add bios registry to /v1/nodes/{node}/bios/{setting}
# v1.75: Add boot_mode, secure_boot fields to node object. # v1.75: Add boot_mode, secure_boot fields to node object.
# v1.76: Add support for changing boot_mode and secure_boot state # 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_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1 MINOR_1_INITIAL_VERSION = 1
@ -192,6 +193,7 @@ MINOR_73_DEPLOY_UNDEPLOY_VERBS = 73
MINOR_74_BIOS_REGISTRY = 74 MINOR_74_BIOS_REGISTRY = 74
MINOR_75_NODE_BOOT_MODE = 75 MINOR_75_NODE_BOOT_MODE = 75
MINOR_76_NODE_CHANGE_BOOT_MODE = 76 MINOR_76_NODE_CHANGE_BOOT_MODE = 76
MINOR_77_DRIVER_FIELDS_SELECTOR = 77
# When adding another version, update: # When adding another version, update:
# - MINOR_MAX_VERSION # - MINOR_MAX_VERSION
@ -199,7 +201,7 @@ MINOR_76_NODE_CHANGE_BOOT_MODE = 76
# explanation of what changed in the new version # explanation of what changed in the new version
# - common/release_mappings.py, RELEASE_MAPPING['master']['api'] # - 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 # String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION) _MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -371,7 +371,7 @@ RELEASE_MAPPING = {
} }
}, },
'master': { 'master': {
'api': '1.76', 'api': '1.77',
'rpc': '1.55', 'rpc': '1.55',
'objects': { 'objects': {
'Allocation': ['1.1'], 'Allocation': ['1.1'],

View File

@ -274,6 +274,77 @@ class TestListDrivers(base.BaseApiTest):
response = self.get_json('/drivers/nope', expect_errors=True) response = self.get_json('/drivers/nope', expect_errors=True)
self.assertEqual(http_client.NOT_FOUND, response.status_int) 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): def _test_links(self, public_url=None):
cfg.CONF.set_override('public_endpoint', public_url, 'api') cfg.CONF.set_override('public_endpoint', public_url, 'api')
self.register_fake_conductors() self.register_fake_conductors()

View File

@ -0,0 +1,9 @@
---
features:
- |
Adds support for fields selector in driver api.
See `story 1674775
<https://storyboard.openstack.org/#!/story/1674775>`_.
* ``GET /v1/drivers?fields=...``
* ``GET /v1/drivers/{driver_name}?fields=...``