Merge "api: Add schema for allocations API (requests)"

This commit is contained in:
Zuul
2025-08-14 17:49:10 +00:00
committed by Gerrit Code Review
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 versions
from ironic.api import method
from ironic.api.schemas.v1 import allocation as schema
from ironic.api import validation
from ironic.common import args
from ironic.common import exception
@@ -237,19 +238,16 @@ class AllocationsController(pecan.rest.RestController):
@METRICS.timer('AllocationsController.get_all')
@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(
min_version=versions.MINOR_52_ALLOCATION,
message=_('The API version does not allow allocations'),
)
@args.validate(node=args.uuid_or_name,
resource_class=args.string,
state=args.string,
marker=args.uuid,
limit=args.integer,
sort_key=args.string,
sort_dir=args.string,
fields=args.string_list,
owner=args.string)
@validation.request_query_schema(schema.index_request_query, None, 59)
@validation.request_query_schema(schema.index_request_query_v60, 60)
def get_all(self, node=None, resource_class=None, state=None, marker=None,
limit=None, sort_key='id', sort_dir='asc', fields=None,
owner=None):
@@ -295,7 +293,13 @@ class AllocationsController(pecan.rest.RestController):
min_version=versions.MINOR_52_ALLOCATION,
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):
"""Retrieve information about the given allocation.
@@ -311,7 +315,6 @@ class AllocationsController(pecan.rest.RestController):
return convert_with_links(rpc_allocation, fields=fields)
def _authorize_create_allocation(self, allocation):
try:
# PRE-RBAC this rule was logically restricted, it is more-unlocked
# 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'),
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):
"""Create a new allocation.
@@ -489,7 +493,9 @@ class AllocationsController(pecan.rest.RestController):
message=_('The API version does not allow updating allocations'),
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):
"""Update an existing allocation.
@@ -533,7 +539,7 @@ class AllocationsController(pecan.rest.RestController):
message=_('The API version does not allow allocations'),
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):
"""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)