api: Add schema for allocations API (requests)

Change-Id: Ida8bd1d098246f6401605b1bd33ef47f82f4154b
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane
2025-03-21 11:03:16 +00:00
parent 53c58dfcc1
commit e22381beda
4 changed files with 246 additions and 14 deletions

View File

@@ -24,6 +24,7 @@ from ironic.api.controllers.v1 import notification_utils as notify
from ironic.api.controllers.v1 import utils as api_utils from ironic.api.controllers.v1 import utils as api_utils
from ironic.api.controllers.v1 import versions from ironic.api.controllers.v1 import versions
from ironic.api import method from ironic.api import method
from ironic.api.schemas.v1 import allocation as schema
from ironic.api import validation from ironic.api import validation
from ironic.common import args from ironic.common import args
from ironic.common import exception from ironic.common import exception
@@ -237,19 +238,16 @@ class AllocationsController(pecan.rest.RestController):
@METRICS.timer('AllocationsController.get_all') @METRICS.timer('AllocationsController.get_all')
@method.expose() @method.expose()
# TODO(stephenfin): We are currently using this for side-effects to e.g.
# convert a CSV string to an array or a string to an integer. We should
# probably rename this decorator or provide a separate, simpler decorator.
@args.validate(limit=args.integer, fields=args.string_list)
@validation.api_version( @validation.api_version(
min_version=versions.MINOR_52_ALLOCATION, min_version=versions.MINOR_52_ALLOCATION,
message=_('The API version does not allow allocations'), message=_('The API version does not allow allocations'),
) )
@args.validate(node=args.uuid_or_name, @validation.request_query_schema(schema.index_request_query, None, 59)
resource_class=args.string, @validation.request_query_schema(schema.index_request_query_v60, 60)
state=args.string,
marker=args.uuid,
limit=args.integer,
sort_key=args.string,
sort_dir=args.string,
fields=args.string_list,
owner=args.string)
def get_all(self, node=None, resource_class=None, state=None, marker=None, def get_all(self, node=None, resource_class=None, state=None, marker=None,
limit=None, sort_key='id', sort_dir='asc', fields=None, limit=None, sort_key='id', sort_dir='asc', fields=None,
owner=None): owner=None):
@@ -295,7 +293,13 @@ class AllocationsController(pecan.rest.RestController):
min_version=versions.MINOR_52_ALLOCATION, min_version=versions.MINOR_52_ALLOCATION,
message=_('The API version does not allow allocations'), message=_('The API version does not allow allocations'),
) )
@args.validate(allocation_ident=args.uuid_or_name, fields=args.string_list) # TODO(stephenfin): We are currently using this for side-effects to e.g.
# convert a CSV string to an array or a string to an integer. We should
# probably rename this decorator or provide a separate, simpler decorator.
@args.validate(fields=args.string_list)
@validation.request_parameter_schema(schema.show_request_parameter)
@validation.request_query_schema(schema.show_request_query, None, 59)
@validation.request_query_schema(schema.show_request_query_v60, 60)
def get_one(self, allocation_ident, fields=None): def get_one(self, allocation_ident, fields=None):
"""Retrieve information about the given allocation. """Retrieve information about the given allocation.
@@ -311,7 +315,6 @@ class AllocationsController(pecan.rest.RestController):
return convert_with_links(rpc_allocation, fields=fields) return convert_with_links(rpc_allocation, fields=fields)
def _authorize_create_allocation(self, allocation): def _authorize_create_allocation(self, allocation):
try: try:
# PRE-RBAC this rule was logically restricted, it is more-unlocked # PRE-RBAC this rule was logically restricted, it is more-unlocked
# post RBAC, but we need to ensure it is not abused. # post RBAC, but we need to ensure it is not abused.
@@ -351,7 +354,8 @@ class AllocationsController(pecan.rest.RestController):
message=_('The API version does not allow allocations'), message=_('The API version does not allow allocations'),
exception_class=webob_exc.HTTPMethodNotAllowed, exception_class=webob_exc.HTTPMethodNotAllowed,
) )
@args.validate(allocation=ALLOCATION_VALIDATOR) @validation.request_body_schema(schema.create_request_body, None, 57)
@validation.request_body_schema(schema.create_request_body_v58, 58)
def post(self, allocation): def post(self, allocation):
"""Create a new allocation. """Create a new allocation.
@@ -489,7 +493,9 @@ class AllocationsController(pecan.rest.RestController):
message=_('The API version does not allow updating allocations'), message=_('The API version does not allow updating allocations'),
exception_class=webob_exc.HTTPMethodNotAllowed, exception_class=webob_exc.HTTPMethodNotAllowed,
) )
@args.validate(allocation_ident=args.string, patch=args.patch) @validation.request_parameter_schema(schema.update_request_parameter)
@validation.request_body_schema(schema.update_request_body, None, 59)
@validation.request_body_schema(schema.update_request_body_v60, 60)
def patch(self, allocation_ident, patch): def patch(self, allocation_ident, patch):
"""Update an existing allocation. """Update an existing allocation.
@@ -533,7 +539,7 @@ class AllocationsController(pecan.rest.RestController):
message=_('The API version does not allow allocations'), message=_('The API version does not allow allocations'),
exception_class=webob_exc.HTTPMethodNotAllowed, exception_class=webob_exc.HTTPMethodNotAllowed,
) )
@args.validate(allocation_ident=args.uuid_or_name) @validation.request_parameter_schema(schema.delete_request_parameter)
def delete(self, allocation_ident): def delete(self, allocation_ident):
"""Delete an allocation. """Delete an allocation.

View File

@@ -0,0 +1,26 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
uuid_or_name = {
'anyOf': [
{'type': 'string', 'format': 'uuid'},
{
'type': 'string',
'minLength': 1,
'maxLength': 255,
'pattern': r'^[a-zA-Z0-9-._~]+$',
},
]
}
sort_dir = {'type': 'string', 'enum': ['asc', 'desc'], 'default': 'asc'}

View File

@@ -0,0 +1,27 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os_traits
STANDARD_TRAITS = os_traits.get_traits()
CUSTOM_TRAIT_PATTERN = "^%s[A-Z0-9_]+$" % os_traits.CUSTOM_NAMESPACE
traits = {
'type': 'string',
'minLength': 1,
'maxLength': 255,
'anyOf': [
{'pattern': CUSTOM_TRAIT_PATTERN},
{'enum': STANDARD_TRAITS},
]
}

View File

@@ -0,0 +1,173 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
from ironic.api.schemas.common import request_types
from ironic.api.schemas.common import response_types
# request parameter schemas
_allocation_request_parameter = {
'type': 'object',
'properties': {
'allocation_ident': {'type': 'string'},
},
'required': ['allocation_ident'],
'additionalProperties': False,
}
show_request_parameter = copy.deepcopy(_allocation_request_parameter)
update_request_parameter = copy.deepcopy(_allocation_request_parameter)
delete_request_parameter = copy.deepcopy(_allocation_request_parameter)
# request query string schemas
index_request_query = {
'type': 'object',
'properties': {
'fields': {
'type': 'array',
'items': {
'enum': [
'candidate_nodes',
'created_at',
'extra',
'last_error',
'links',
'name',
'node_uuid',
'resource_class',
'state',
'traits',
'updated_at',
'uuid',
],
},
# OpenAPI-specific properties
# https://swagger.io/docs/specification/v3_0/serialization/#query-parameters
'style': 'form',
'explode': False,
},
'limit': {'type': 'integer'},
'marker': {'type': 'string', 'format': 'uuid'},
'node': request_types.uuid_or_name,
'owner': {'type': 'string'},
'resource_class': {'type': 'string'},
'sort_dir': request_types.sort_dir,
# TODO(stephenfin): This could probably be narrower but we need to be
# careful not to change the response type. If we do, a new microversion
# will be needed.
'sort_key': {'type': 'string'},
'state': {'type': 'string'},
},
'required': [],
'additionalProperties': False,
}
index_request_query_v60 = copy.deepcopy(index_request_query)
index_request_query_v60['properties']['fields']['items']['enum'].append(
'owner'
)
show_request_query = {
'type': 'object',
'properties': {
'fields': {
'type': 'array',
'items': {
'enum': [
'candidate_nodes',
'created_at',
'extra',
'last_error',
'links',
'name',
'node_uuid',
'resource_class',
'state',
'traits',
'updated_at',
'uuid',
],
},
# OpenAPI-specific properties
# https://swagger.io/docs/specification/v3_0/serialization/#query-parameters
'style': 'form',
'explode': False,
},
},
'required': [],
'additionalProperties': False,
}
show_request_query_v60 = copy.deepcopy(show_request_query)
show_request_query_v60['properties']['fields']['items']['enum'].append(
'owner'
)
# request body schemas
create_request_body = {
'type': 'object',
'properties': {
'candidate_nodes': {
'type': ['array', 'null'],
'items': request_types.uuid_or_name,
},
'extra': {'type': ['object', 'null']},
# TODO(stephenfin): We'd like to use request_types.uuid_or_name here
# but doing so will change the error response
'name': {'type': ['string', 'null']},
# TODO(stephenfin): The docs say that owner is only present in v1.60+,
# but I can't see anything in the code to prevent this in the POST
# request, only in the GET request and all responses
'owner': {'type': ['string', 'null']},
'resource_class': {'type': ['string', 'null'], 'maxLength': 80},
'traits': {
'type': ['array', 'null'],
'items': response_types.traits,
},
'uuid': {'type': ['string', 'null']},
},
# TODO(stephenfin): The resource_class field is required when node is not
# provided. We'd like to express this here, but doing so will change the
# error response.
'required': [],
'additionalProperties': False,
}
create_request_body_v58 = copy.deepcopy(create_request_body)
create_request_body_v58['properties'].update({
'node': {'type': ['string', 'null']},
})
# TODO(stephenfin): This needs to be completed. We probably want a helper to
# generate these since they are superficially identical, with only the allowed
# patch fields changing
update_request_body = {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'op': {'enum': ['add', 'replace', 'remove']},
'path': {'type': 'string'},
'value': {'type': ['string', 'object', 'null']},
},
'required': ['op', 'path'],
'additionalProperties': False,
},
}
# TODO(stephenfin): The code suggests that we should be allowing 'owner' here,
# but it's not included in PATCH_ALLOWED_FIELDS so I have ignored it for now
update_request_body_v60 = copy.deepcopy(update_request_body)