Skip to content

Commit

Permalink
Merge pull request #250 from Cray-HPE/release-2.15.0
Browse files Browse the repository at this point in the history
Release 2.15.0
  • Loading branch information
mharding-hpe authored Feb 13, 2024
2 parents a21e03e + a0d7c53 commit e0935e5
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 32 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.15.0] - 2024-02-13
### Changed
- Updated API spec to reflect the actual API behavior: a component filter must have exactly one
property specified (`session` or `ids`), but not both.
- Modified API server for `v2/components`:
- Add minor debug logging to match what is done in `v2/sessions` methods
- Validate incoming component put/patch requests against the schema
- Gracefully handle the case where a components filter includes nonexistent component IDs
(instead of returning with a 500 internal server error)

## [2.14.0] - 2024-02-06
### Added
- Scalable Boot Provisioning Service (SBPS) support
Expand Down
29 changes: 25 additions & 4 deletions api/openapi.yaml.in
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ components:
type: string
description: An empty string value.
enum: [ '' ]
EmptyStringNullable:
type: string
description: An empty string value.
enum: [ '' ]
EnableCfs:
type: boolean
description: |
Expand Down Expand Up @@ -1528,10 +1532,9 @@ components:
type: array
items:
$ref: '#/components/schemas/V2Component'
V2ComponentsFilter:
V2ComponentsFilterByIds:
description: |
Information for patching multiple Components.
If a Session name is specified, then all Components part of this Session will be patched.
Information for patching multiple Components by listing their IDs.
type: object
properties:
ids:
Expand All @@ -1543,17 +1546,35 @@ components:

This restriction is not enforced in this version of BOS, but it is
targeted to start being enforced in an upcoming BOS version.
minLength: 1
session:
$ref: '#/components/schemas/EmptyStringNullable'
required: [ids]
additionalProperties: false
V2ComponentsFilterBySession:
description: |
Information for patching multiple Components by Session name.
All Components part of this Session will be patched.
type: object
properties:
ids:
$ref: '#/components/schemas/EmptyStringNullable'
session:
$ref: '#/components/schemas/V2SessionName'
required: [session]
additionalProperties: false
V2ComponentsUpdate:
description: Information for patching multiple Components.
type: object
properties:
patch:
$ref: '#/components/schemas/V2Component'
filters:
$ref: '#/components/schemas/V2ComponentsFilter'
oneOf:
- $ref: '#/components/schemas/V2ComponentsFilterByIds'
- $ref: '#/components/schemas/V2ComponentsFilterBySession'
required: [patch, filters]
additionalProperties: false
V2ApplyStagedComponents:
description: |
A list of Components that should have their staged Session applied.
Expand Down
132 changes: 104 additions & 28 deletions src/bos/server/controllers/v2/components.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# MIT License
#
# (C) Copyright 2021-2023 Hewlett Packard Enterprise Development LP
# (C) Copyright 2021-2024 Hewlett Packard Enterprise Development LP
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
Expand Down Expand Up @@ -30,6 +30,9 @@
from bos.server import redis_db_utils as dbutils
from bos.server.controllers.v2.options import get_v2_options_data
from bos.server.dbs.boot_artifacts import get_boot_artifacts, BssTokenUnknown
from bos.server.models.v2_component import V2Component as Component # noqa: E501
from bos.server.models.v2_component_array import V2ComponentArray as ComponentArray # noqa: E501
from bos.server.models.v2_components_update import V2ComponentsUpdate as ComponentsUpdate # noqa: E501

LOGGER = logging.getLogger('bos.server.controllers.v2.components')
DB = dbutils.get_wrapper(db='components')
Expand Down Expand Up @@ -156,16 +159,34 @@ def _matches_filter(data, enabled, session, staged_session, phase, status):
def put_v2_components():
"""Used by the PUT /components API operation"""
LOGGER.debug("PUT /components invoked put_components")
if not connexion.request.is_json:
msg = "Must be in JSON format"
LOGGER.error(msg)
return msg, 400

LOGGER.debug("connexion.request.is_json")
data=connexion.request.get_json()
LOGGER.debug("type=%s", type(data))
LOGGER.debug("Received: %s", data)

try:
data = connexion.request.get_json()
components = []
for component_data in data:
component_id = component_data['id']
components.append((component_id, component_data))
# This call is just to ensure that the data
# coming in is valid per the API schema
ComponentArray.from_dict(data) # noqa: E501
except Exception as err:
return connexion.problem(
status=400, title="Error parsing the data provided.",
detail=str(err))
msg="Provided data does not follow API spec"
LOGGER.exception(msg)
return connexion.problem(status=400, title=msg,detail=str(err))

components = []
for component_data in data:
try:
component_id = component_data['id']
except KeyError:
return connexion.problem(
status=400, title="Required field missing.",
detail="At least one component is missing the required 'id' field")
components.append((component_id, component_data))
response = []
for component_id, component_data in components:
component_data = _set_auto_fields(component_data)
Expand All @@ -178,15 +199,40 @@ def put_v2_components():
def patch_v2_components():
"""Used by the PATCH /components API operation"""
LOGGER.debug("PATCH /components invoked patch_components")
data = connexion.request.get_json()
if not connexion.request.is_json:
msg = "Must be in JSON format"
LOGGER.error(msg)
return msg, 400

LOGGER.debug("connexion.request.is_json")
data=connexion.request.get_json()
LOGGER.debug("type=%s", type(data))
LOGGER.debug("Received: %s", data)

if type(data) == list:
try:
# This call is just to ensure that the data
# coming in is valid per the API schema
ComponentArray.from_dict(data) # noqa: E501
except Exception as err:
msg="Provided data does not follow API spec"
LOGGER.exception(msg)
return connexion.problem(status=400, title=msg,detail=str(err))
return patch_v2_components_list(data)
elif type(data) == dict:
try:
# This call is just to ensure that the data
# coming in is valid per the API schema
ComponentsUpdate.from_dict(data) # noqa: E501
except Exception as err:
msg="Provided data does not follow API spec"
LOGGER.exception(msg)
return connexion.problem(status=400, title=msg,detail=str(err))
return patch_v2_components_dict(data)
else:
return connexion.problem(
status=400, title="Error parsing the data provided.",
detail="Unexpected data type {}".format(str(type(data))))

return connexion.problem(
status=400, title="Error parsing the data provided.",
detail="Unexpected data type {}".format(str(type(data))))


def patch_v2_components_list(data):
Expand Down Expand Up @@ -227,18 +273,23 @@ def patch_v2_components_dict(data):
return connexion.problem(
status=400, title="Error parsing the ids provided.",
detail=str(err))
# Make sure all of the components exist and belong to this tenant (if any)
for component_id in id_list:
if component_id not in DB or not _is_valid_tenant_component(component_id):
return connexion.problem(
status=404, title="Component not found.",
detail="Component {} could not be found".format(component_id))
elif session:
id_list = [component["id"] for component in get_v2_components_data(session=session)]
id_list = [component["id"] for component in get_v2_components_data(session=session, tenant=get_tenant_from_header())]
else:
return connexion.problem(
status=400, title="One filter must be provided.",
detail="Only one filter may be provided.")
status=400, title="Exactly one filter must be provided.",
detail="Exactly one filter may be provided.")
response = []
patch = data.get("patch")
if "id" in patch:
del patch["id"]
patch = _set_auto_fields(patch)
id_list, _ = _apply_tenant_limit(id_list)
for component_id in id_list:
response.append(DB.patch(component_id, patch, _update_handler))
return response, 200
Expand All @@ -263,12 +314,24 @@ def get_v2_component(component_id):
def put_v2_component(component_id):
"""Used by the PUT /components/{component_id} API operation"""
LOGGER.debug("PUT /components/id invoked put_component")
if not connexion.request.is_json:
msg = "Must be in JSON format"
LOGGER.error(msg)
return msg, 400

LOGGER.debug("connexion.request.is_json")
data=connexion.request.get_json()
LOGGER.debug("type=%s", type(data))
LOGGER.debug("Received: %s", data)

try:
data = connexion.request.get_json()
# This call is just to ensure that the data
# coming in is valid per the API schema
Component.from_dict(data) # noqa: E501
except Exception as err:
return connexion.problem(
status=400, title="Error parsing the data provided.",
detail=str(err))
msg="Provided data does not follow API spec"
LOGGER.exception(msg)
return connexion.problem(status=400, title=msg,detail=str(err))
data['id'] = component_id
data = _set_auto_fields(data)
return DB.put(component_id, data), 200
Expand All @@ -279,16 +342,29 @@ def put_v2_component(component_id):
def patch_v2_component(component_id):
"""Used by the PATCH /components/{component_id} API operation"""
LOGGER.debug("PATCH /components/id invoked patch_component")
if not connexion.request.is_json:
msg = "Must be in JSON format"
LOGGER.error(msg)
return msg, 400

LOGGER.debug("connexion.request.is_json")
data=connexion.request.get_json()
LOGGER.debug("type=%s", type(data))
LOGGER.debug("Received: %s", data)

try:
# This call is just to ensure that the data
# coming in is valid per the API schema
Component.from_dict(data) # noqa: E501
except Exception as err:
msg="Provided data does not follow API spec"
LOGGER.exception(msg)
return connexion.problem(status=400, title=msg,detail=str(err))

if component_id not in DB or not _is_valid_tenant_component(component_id):
return connexion.problem(
status=404, title="Component not found.",
detail="Component {} could not be found".format(component_id))
try:
data = connexion.request.get_json()
except Exception as err:
return connexion.problem(
status=400, title="Error parsing the data provided.",
detail=str(err))
if "actual_state" in data and not validate_actual_state_change_is_allowed(component_id):
return connexion.problem(
status=409, title="Actual state can not be updated.",
Expand Down

0 comments on commit e0935e5

Please sign in to comment.