diff --git a/CHANGELOG.md b/CHANGELOG.md index 540aff66..d01be6b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.17.7] - 2024-05-16 +### Changed +- Added more checks to avoid operating on empty lists +- Compact response bodies to single line before logging them +- Improve BOS logging of unexpected errors + ## [2.17.6] - 2024-04-19 ### Fixed - Corrected description of `disable_components_on_completion` in API spec. diff --git a/src/bos/common/tenant_utils.py b/src/bos/common/tenant_utils.py index 03498478..0ed4b19f 100644 --- a/src/bos/common/tenant_utils.py +++ b/src/bos/common/tenant_utils.py @@ -27,7 +27,7 @@ import logging import hashlib from requests.exceptions import HTTPError -from bos.common.utils import requests_retry_session, PROTOCOL +from bos.common.utils import exc_type_msg, requests_retry_session, PROTOCOL LOGGER = logging.getLogger('bos.common.tenant_utils') @@ -78,7 +78,7 @@ def get_tenant_data(tenant, session=None): try: response.raise_for_status() except HTTPError as e: - LOGGER.error("Failed getting tenant data from tapms: %s", e) + LOGGER.error("Failed getting tenant data from tapms: %s", exc_type_msg(e)) if response.status_code == 404: raise InvalidTenantException(f"Data not found for tenant {tenant}") from e else: @@ -110,6 +110,7 @@ def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except InvalidTenantException as e: + LOGGER.debug("Invalid tenant: %s", exc_type_msg(e)) return connexion.problem( status=400, title='Invalid tenant', detail=str(e)) @@ -122,6 +123,7 @@ def reject_invalid_tenant(func): def wrapper(*args, **kwargs): tenant = get_tenant_from_header() if tenant and not validate_tenant_exists(tenant): + LOGGER.debug("The provided tenant does not exist") return connexion.problem( status=400, title="Invalid tenant", detail=str("The provided tenant does not exist")) diff --git a/src/bos/common/utils.py b/src/bos/common/utils.py index 13d09d5f..98256b86 100644 --- a/src/bos/common/utils.py +++ b/src/bos/common/utils.py @@ -1,7 +1,7 @@ # # MIT License # -# (C) Copyright 2023 Hewlett Packard Enterprise Development LP +# (C) Copyright 2022-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"), @@ -23,6 +23,7 @@ # import datetime import re +import traceback from dateutil.parser import parse import requests from requests.adapters import HTTPAdapter @@ -59,6 +60,7 @@ def duration_to_timedelta(timestamp: str): seconds = timeval * seconds_table[durationval] return datetime.timedelta(seconds=seconds) + class TimeoutHTTPAdapter(HTTPAdapter): """ An HTTP Adapter that allows a session level timeout for both read and connect attributes. This prevents interruption @@ -95,3 +97,21 @@ def requests_retry_session(retries=10, backoff_factor=0.5, # Mounting to only http will not work! session.mount("%s://" % protocol, adapter) return session + + +def compact_response_text(response_text: str) -> str: + """ + Often JSON is "pretty printed" in response text, which is undesirable for our logging. + This function transforms the response text into a single line, stripping leading and trailing whitespace from each line, + and then returns it. + """ + if response_text: + return ' '.join([ line.strip() for line in response_text.split('\n') ]) + return str(response_text) + + +def exc_type_msg(exc: Exception) -> str: + """ + Given an exception, returns a string of its type and its text (e.g. TypeError: 'int' object is not subscriptable) + """ + return ''.join(traceback.format_exception_only(type(exc), exc)) diff --git a/src/bos/operators/base.py b/src/bos/operators/base.py index 1c988203..7448a658 100644 --- a/src/bos/operators/base.py +++ b/src/bos/operators/base.py @@ -33,6 +33,7 @@ import time from typing import List, NoReturn, Type +from bos.common.utils import exc_type_msg from bos.common.values import Status from bos.operators.filters.base import BaseFilter from bos.operators.utils.clients.bos.options import options @@ -140,6 +141,10 @@ def _get_components(self) -> List[dict]: def _handle_failed_components(self, components: List[dict]) -> List[dict]: """ Marks components failed if the retry limits are exceeded """ + if not components: + # If we have been passed an empty list, there is nothing to do. + LOGGER.debug("_handle_failed_components: No components to handle") + return [] failed_components = [] good_components = [] # Any component that isn't determined to be in a failed state for component in components: @@ -163,6 +168,10 @@ def _update_database(self, components: List[dict], additional_fields: dict=None) Updates the BOS database for all components acted on by the operator Includes updating the last action, attempt count and error """ + if not components: + # If we have been passed an empty list, there is nothing to do. + LOGGER.debug("_update_database: No components require database updates") + return data = [] for component in components: patch = { @@ -200,6 +209,10 @@ def _preset_last_action(self, components: List[dict]) -> None: # e.g. nodes could be powered-on without the correct power-on last action, causing status problems if not self.name: return + if not components: + # If we have been passed an empty list, there is nothing to do. + LOGGER.debug("_preset_last_action: No components require database updates") + return data = [] for component in components: patch = { @@ -221,6 +234,10 @@ def _update_database_for_failure(self, components: List[dict]) -> None: """ Updates the BOS database for all components the operator believes have failed """ + if not components: + # If we have been passed an empty list, there is nothing to do. + LOGGER.debug("_update_database_for_failure: No components require database updates") + return data = [] for component in components: patch = { @@ -250,7 +267,7 @@ def _update_log_level() -> None: LOGGER.log(new_level, 'Logging level changed from {} to {}'.format( logging.getLevelName(current_level), logging.getLevelName(new_level))) except Exception as e: - LOGGER.error('Error updating logging level: {}'.format(e)) + LOGGER.error('Error updating logging level: %s', exc_type_msg(e)) def _liveliness_heartbeat() -> NoReturn: diff --git a/src/bos/operators/configuration.py b/src/bos/operators/configuration.py index f4424029..decef243 100644 --- a/src/bos/operators/configuration.py +++ b/src/bos/operators/configuration.py @@ -2,7 +2,7 @@ # # MIT License # -# (C) Copyright 2022 Hewlett Packard Enterprise Development LP +# (C) Copyright 2022, 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"), @@ -57,7 +57,8 @@ def filters(self): ] def _act(self, components): - set_cfs(components, enabled=True) + if components: + set_cfs(components, enabled=True) return components diff --git a/src/bos/operators/power_off_forceful.py b/src/bos/operators/power_off_forceful.py index 5683bb84..93f0b0b9 100644 --- a/src/bos/operators/power_off_forceful.py +++ b/src/bos/operators/power_off_forceful.py @@ -2,7 +2,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"), @@ -57,8 +57,9 @@ def filters(self): ] def _act(self, components): - component_ids = [component['id'] for component in components] - pcs.force_off(nodes=component_ids) + if components: + component_ids = [component['id'] for component in components] + pcs.force_off(nodes=component_ids) return components diff --git a/src/bos/operators/power_off_graceful.py b/src/bos/operators/power_off_graceful.py index 5d16ccc5..c120fa4e 100644 --- a/src/bos/operators/power_off_graceful.py +++ b/src/bos/operators/power_off_graceful.py @@ -2,7 +2,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"), @@ -53,8 +53,9 @@ def filters(self): ] def _act(self, components): - component_ids = [component['id'] for component in components] - pcs.soft_off(component_ids) + if components: + component_ids = [component['id'] for component in components] + pcs.soft_off(component_ids) return components diff --git a/src/bos/operators/power_on.py b/src/bos/operators/power_on.py index 0662d9e4..414ed939 100644 --- a/src/bos/operators/power_on.py +++ b/src/bos/operators/power_on.py @@ -26,6 +26,7 @@ import logging from requests import HTTPError +from bos.common.utils import exc_type_msg from bos.common.values import Action, Status import bos.operators.utils.clients.bss as bss import bos.operators.utils.clients.pcs as pcs @@ -59,6 +60,8 @@ def filters(self): ] def _act(self, components): + if not components: + return components self._preset_last_action(components) try: self._set_bss(components) @@ -85,6 +88,10 @@ def _set_bss(self, components, retries=5): Because the connection to the BSS tokens database can be lost due to infrequent use, retry up to retries number of times. """ + if not components: + # If we have been passed an empty list, there is nothing to do. + LOGGER.debug("_set_bss: No components to act on") + return parameters = defaultdict(set) sessions = {} for component in components: @@ -106,9 +113,9 @@ def _set_bss(self, components, retries=5): resp = bss.set_bss(node_set=nodes, kernel_params=kernel_parameters, kernel=kernel, initrd=initrd) resp.raise_for_status() - except HTTPError: - LOGGER.error(f"Failed to set BSS for boot artifacts: {key} for" - "nodes: {nodes}. Error: {err}") + except HTTPError as err: + LOGGER.error("Failed to set BSS for boot artifacts: %s for nodes: %s. Error: %s", + key, nodes, exc_type_msg(err)) else: token = resp.headers['bss-referral-token'] attempts = 0 @@ -118,7 +125,8 @@ def _set_bss(self, components, retries=5): break except Exception as err: attempts += 1 - LOGGER.error(f"An error occurred attempting to record the BSS token: {err}") + LOGGER.error("An error occurred attempting to record the BSS token: %s", + exc_type_msg(err)) if attempts > retries: raise LOGGER.info("Retrying to record the BSS token.") @@ -128,6 +136,8 @@ def _set_bss(self, components, retries=5): "desired_state": {"bss_token": token}, "session": sessions[node]}) LOGGER.info('Found %d components that require BSS token updates', len(bss_tokens)) + if not bss_tokens: + return redacted_component_updates = [ { "id": comp["id"], "session": comp["session"] @@ -136,7 +146,6 @@ def _set_bss(self, components, retries=5): LOGGER.debug('Updated components (minus desired_state data): {}'.format(redacted_component_updates)) self.bos_client.components.update_components(bss_tokens) - if __name__ == '__main__': main(PowerOnOperator) diff --git a/src/bos/operators/session_setup.py b/src/bos/operators/session_setup.py index 50bd40d6..06fea441 100644 --- a/src/bos/operators/session_setup.py +++ b/src/bos/operators/session_setup.py @@ -36,6 +36,7 @@ from bos.operators.utils.clients.bos.options import options from bos.operators.utils.rootfs.factory import ProviderFactory from bos.operators.session_completion import SessionCompletionOperator +from bos.common.utils import exc_type_msg from bos.common.values import Action, EMPTY_ACTUAL_STATE, EMPTY_DESIRED_STATE, EMPTY_STAGED_STATE from bos.common.tenant_utils import get_tenant_component_set, InvalidTenantException @@ -135,7 +136,7 @@ def _setup_components(self): if not all_component_ids: raise SessionSetupException("No nodes were found to act upon.") except Exception as err: - raise SessionSetupException(err) + raise SessionSetupException(err) from err else: self._log(LOGGER.info, 'Found %d components that require updates', len(data)) self._log(LOGGER.debug, f'Updated components: {data}') @@ -166,20 +167,26 @@ def _get_boot_set_component_list(self, boot_set) -> Set[str]: self._log(LOGGER.warning, f"No hardware matching role {role_name}") continue nodes |= self.inventory.roles[role_name] + if not nodes: + self._log(LOGGER.warning, "After populating node list, before any filtering, no nodes to act upon.") + return nodes + self._log(LOGGER.debug, "Before any limiting or filtering, %d nodes to act upon.", len(nodes)) # Filter out any nodes that do not match the boot set architecture desired; boot sets that do not have a # specified arch are considered 'X86' nodes. arch = boot_set.get('arch', 'X86') nodes = self._apply_arch(nodes, arch) + if not nodes: + return nodes # Filter to nodes defined by limit nodes = self._apply_limit(nodes) + if not nodes: + return nodes # Exclude disabled nodes - include_disabled = self.session_data.get("include_disabled", False) - if not include_disabled: - hsmfilter = HSMState(enabled=True) - nodes = set(hsmfilter._filter(list(nodes))) - nodes = self._apply_tenant_limit(nodes) + nodes = self._apply_include_disabled(nodes) if not nodes: - self._log(LOGGER.warning, "No nodes were found to act upon.") + return nodes + # If this session is for a tenant, filter out nodes not belonging to this tenant + nodes = self._apply_tenant_limit(nodes) return nodes def _apply_arch(self, nodes, arch): @@ -204,7 +211,29 @@ def _apply_arch(self, nodes, arch): if arch == 'X86': valid_archs.add('UNKNOWN') hsm_filter = HSMState() - return set(hsm_filter.filter_by_arch(nodes, valid_archs)) + nodes = set(hsm_filter.filter_by_arch(nodes, valid_archs)) + if not nodes: + self._log(LOGGER.warning, "After filtering for architecture, no nodes remain to act upon.") + else: + self._log(LOGGER.debug, "After filtering for architecture, %d nodes remain to act upon.", len(nodes)) + return nodes + + def _apply_include_disabled(self, nodes): + """ + If include_disabled is False for this session, filter out any nodes which are disabled in HSM. + If include_disabled is True, return the node list unchanged. + """ + include_disabled = self.session_data.get("include_disabled", False) + if include_disabled: + # Nodes disabled in HSM may be included, so no filtering is required + return nodes + hsmfilter = HSMState(enabled=True) + nodes = set(hsmfilter._filter(list(nodes))) + if not nodes: + self._log(LOGGER.warning, "After removing disabled nodes, no nodes remain to act upon.") + else: + self._log(LOGGER.debug, "After removing disabled nodes, %d nodes remain to act upon.", len(nodes)) + return nodes def _apply_limit(self, nodes): session_limit = self.session_data.get('limit') @@ -230,6 +259,10 @@ def _apply_limit(self, nodes): limit_nodes = self.inventory[limit] limit_node_set = op(limit_nodes) nodes = nodes.intersection(limit_node_set) + if not nodes: + self._log(LOGGER.warning, "After applying limit, no nodes remain to act upon.") + else: + self._log(LOGGER.debug, "After applying limit, %d nodes remain to act upon.", len(nodes)) return nodes def _apply_tenant_limit(self, nodes): @@ -241,6 +274,10 @@ def _apply_tenant_limit(self, nodes): except InvalidTenantException as e: raise SessionSetupException(str(e)) from e nodes = nodes.intersection(tenant_limit) + if not nodes: + self._log(LOGGER.warning, "After applying tenant limit, no nodes remain to act upon.") + else: + self._log(LOGGER.debug, "After applying tenant limit, %d nodes remain to act upon.", len(nodes)) return nodes def _mark_running(self, component_ids): @@ -377,7 +414,7 @@ def assemble_kernel_boot_parameters(self, boot_set, artifact_info): except (ClientError, UnicodeDecodeError, S3ObjectNotFound) as error: self._log(LOGGER.error, "Unable to read file {}. Thus, no kernel boot parameters obtained " "from image".format(artifact_info['boot_parameters'])) - LOGGER.error(error) + LOGGER.error(exc_type_msg(error)) raise # Parameters from the BOS Session template if the parameters exist. diff --git a/src/bos/operators/utils/boot_image_metadata/s3_boot_image_metadata.py b/src/bos/operators/utils/boot_image_metadata/s3_boot_image_metadata.py index 755299e1..ff3f52fd 100644 --- a/src/bos/operators/utils/boot_image_metadata/s3_boot_image_metadata.py +++ b/src/bos/operators/utils/boot_image_metadata/s3_boot_image_metadata.py @@ -1,7 +1,7 @@ # # MIT License # -# (C) Copyright 2021-2022 Hewlett Packard Enterprise Development LP +# (C) Copyright 2021-2022, 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"), @@ -25,8 +25,9 @@ from botocore.exceptions import ClientError -from . import BootImageMetaData, BootImageMetaDataBadRead -from ..clients.s3 import S3BootArtifacts, S3MissingConfiguration, ArtifactNotFound +from bos.common.utils import exc_type_msg +from bos.operators.utils.boot_image_metadata import BootImageMetaData, BootImageMetaDataBadRead +from bos.operators.utils.clients.s3 import S3BootArtifacts, S3MissingConfiguration, ArtifactNotFound LOGGER = logging.getLogger('bos.operators.utils.boot_image_metadata.s3_boot_image_metadata') @@ -45,27 +46,27 @@ def __init__(self, boot_set): try: self.artifact_summary['kernel'] = self.kernel_path except ArtifactNotFound as err: - LOGGER.warn(err) + LOGGER.warn(exc_type_msg(err)) try: self.artifact_summary['initrd'] = self.initrd_path except ArtifactNotFound as err: - LOGGER.warn(err) + LOGGER.warn(exc_type_msg(err)) try: self.artifact_summary['rootfs'] = self.rootfs_path except ArtifactNotFound as err: - LOGGER.warn(err) + LOGGER.warn(exc_type_msg(err)) try: self.artifact_summary['rootfs_etag'] = self.rootfs_etag except ArtifactNotFound as err: - LOGGER.warn(err) + LOGGER.warn(exc_type_msg(err)) try: self.artifact_summary['boot_parameters'] = self.boot_parameters_path except ArtifactNotFound as err: - LOGGER.warn(err) + LOGGER.warn(exc_type_msg(err)) try: self.artifact_summary['boot_parameters_etag'] = self.boot_parameters_etag except ArtifactNotFound as err: - LOGGER.warn(err) + LOGGER.warn(exc_type_msg(err)) @property def metadata(self): @@ -79,7 +80,7 @@ def metadata(self): try: return self.boot_artifacts.manifest_json except (ClientError, S3MissingConfiguration) as error: - LOGGER.error("Unable to read %s -- Error: %s", self._boot_set.get('path', ''), error) + LOGGER.error("Unable to read %s -- Error: %s", self._boot_set.get('path', ''), exc_type_msg(error)) raise BootImageMetaDataBadRead(error) @property diff --git a/src/bos/operators/utils/clients/bos/base.py b/src/bos/operators/utils/clients/bos/base.py index c3a89422..23b41e03 100644 --- a/src/bos/operators/utils/clients/bos/base.py +++ b/src/bos/operators/utils/clients/bos/base.py @@ -27,7 +27,7 @@ from urllib3.exceptions import MaxRetryError from bos.common.tenant_utils import get_new_tenant_header -from bos.common.utils import PROTOCOL, requests_retry_session +from bos.common.utils import PROTOCOL, exc_type_msg, requests_retry_session LOGGER = logging.getLogger('bos.operators.utils.clients.bos.base') @@ -43,13 +43,13 @@ def wrap(*args, **kwargs): result = func(*args, **kwargs) return result except (ConnectionError, MaxRetryError) as e: - LOGGER.error("Unable to connect to BOS: {}".format(e)) + LOGGER.error("Unable to connect to BOS: %s", exc_type_msg(e)) raise e except HTTPError as e: - LOGGER.error("Unexpected response from BOS: {}".format(e)) + LOGGER.error("Unexpected response from BOS: %s", exc_type_msg(e)) raise e except json.JSONDecodeError as e: - LOGGER.error("Non-JSON response from BOS: {}".format(e)) + LOGGER.error("Non-JSON response from BOS: %s", exc_type_msg(e)) raise e return wrap diff --git a/src/bos/operators/utils/clients/bos/options.py b/src/bos/operators/utils/clients/bos/options.py index cef51106..df06a4da 100644 --- a/src/bos/operators/utils/clients/bos/options.py +++ b/src/bos/operators/utils/clients/bos/options.py @@ -26,7 +26,7 @@ from requests.exceptions import HTTPError, ConnectionError from urllib3.exceptions import MaxRetryError -from bos.common.utils import requests_retry_session +from bos.common.utils import exc_type_msg, requests_retry_session from bos.operators.utils.clients.bos.base import BASE_ENDPOINT LOGGER = logging.getLogger('bos.operators.utils.clients.bos.options') @@ -56,11 +56,11 @@ def _get_options(self): response.raise_for_status() return json.loads(response.text) except (ConnectionError, MaxRetryError) as e: - LOGGER.error("Unable to connect to BOS: {}".format(e)) + LOGGER.error("Unable to connect to BOS: %s", exc_type_msg(e)) except HTTPError as e: - LOGGER.error("Unexpected response from BOS: {}".format(e)) + LOGGER.error("Unexpected response from BOS: %s", exc_type_msg(e)) except json.JSONDecodeError as e: - LOGGER.error("Non-JSON response from BOS: {}".format(e)) + LOGGER.error("Non-JSON response from BOS: %s", exc_type_msg(e)) return {} def get_option(self, key, value_type, default): diff --git a/src/bos/operators/utils/clients/bss.py b/src/bos/operators/utils/clients/bss.py index 0f815e41..885a8639 100644 --- a/src/bos/operators/utils/clients/bss.py +++ b/src/bos/operators/utils/clients/bss.py @@ -25,7 +25,7 @@ import logging import json -from bos.common.utils import requests_retry_session, PROTOCOL +from bos.common.utils import compact_response_text, exc_type_msg, requests_retry_session, PROTOCOL LOGGER = logging.getLogger(__name__) SERVICE_NAME = 'cray-bss' @@ -58,13 +58,16 @@ def set_bss(node_set, kernel_params, kernel, initrd, session=None): communicating with the Hardware State Manager ''' + if not node_set: + # Cannot simply return if no nodes are specified, as this function + # is intended to return the response object from BSS. + # Accordingly, an Exception is raised. + raise Exception("set_bss called with empty node_set") + session = session or requests_retry_session() LOGGER.info("Params: {}".format(kernel_params)) url = "%s/bootparameters" % (ENDPOINT) - if not node_set: - return - # Assignment payload payload = {"hosts": list(node_set), "params": kernel_params, @@ -75,9 +78,9 @@ def set_bss(node_set, kernel_params, kernel, initrd, session=None): try: resp = session.put(url, data=json.dumps(payload), verify=False) LOGGER.debug("Response status code=%d, reason=%s, body=%s", resp.status_code, - resp.reason, resp.text) + resp.reason, compact_response_text(resp.text)) resp.raise_for_status() return resp except HTTPError as err: - LOGGER.error("%s" % err) + LOGGER.error(exc_type_msg(err)) raise diff --git a/src/bos/operators/utils/clients/cfs.py b/src/bos/operators/utils/clients/cfs.py index b31ee93d..0af1b2e3 100644 --- a/src/bos/operators/utils/clients/cfs.py +++ b/src/bos/operators/utils/clients/cfs.py @@ -25,7 +25,7 @@ import logging from requests.exceptions import HTTPError, ConnectionError -from bos.common.utils import requests_retry_session, PROTOCOL +from bos.common.utils import compact_response_text, exc_type_msg, requests_retry_session, PROTOCOL SERVICE_NAME = 'cray-cfs-api' BASE_ENDPOINT = "%s://%s/v3" % (PROTOCOL, SERVICE_NAME) @@ -51,11 +51,11 @@ def get_components(session=None, **params): LOGGER.debug("GET %s with params=%s", COMPONENTS_ENDPOINT, params) response = session.get(COMPONENTS_ENDPOINT, params=params) LOGGER.debug("Response status code=%d, reason=%s, body=%s", response.status_code, - response.reason, response.text) + response.reason, compact_response_text(response.text)) try: response.raise_for_status() except HTTPError as err: - LOGGER.error("Failed getting nodes from CFS: %s", err) + LOGGER.error("Failed getting nodes from CFS: %s", exc_type_msg(err)) raise response_json = response.json() new_components = response_json["components"] @@ -75,11 +75,11 @@ def patch_components(data, session=None): LOGGER.debug("PATCH %s with body=%s", COMPONENTS_ENDPOINT, data) response = session.patch(COMPONENTS_ENDPOINT, json=data) LOGGER.debug("Response status code=%d, reason=%s, body=%s", response.status_code, - response.reason, response.text) + response.reason, compact_response_text(response.text)) try: response.raise_for_status() except HTTPError as err: - LOGGER.error("Failed asking CFS to configure nodes: %s", err) + LOGGER.error("Failed asking CFS to configure nodes: %s", exc_type_msg(err)) raise diff --git a/src/bos/operators/utils/clients/hsm.py b/src/bos/operators/utils/clients/hsm.py index 860f8430..a1b26c3c 100644 --- a/src/bos/operators/utils/clients/hsm.py +++ b/src/bos/operators/utils/clients/hsm.py @@ -28,7 +28,7 @@ from requests.exceptions import HTTPError, ConnectionError from urllib3.exceptions import MaxRetryError -from bos.common.utils import requests_retry_session, PROTOCOL +from bos.common.utils import compact_response_text, exc_type_msg, requests_retry_session, PROTOCOL SERVICE_NAME = 'cray-smd' BASE_ENDPOINT = "%s://%s/hsm/v2/" % (PROTOCOL, SERVICE_NAME) @@ -59,14 +59,14 @@ def read_all_node_xnames(): try: response = session.get(endpoint) except ConnectionError as ce: - LOGGER.error("Unable to contact HSM service: %s", ce) + LOGGER.error("Unable to contact HSM service: %s", exc_type_msg(ce)) raise HWStateManagerException(ce) from ce LOGGER.debug("Response status code=%d, reason=%s, body=%s", response.status_code, - response.reason, response.text) + response.reason, compact_response_text(response.text)) try: response.raise_for_status() except (HTTPError, MaxRetryError) as hpe: - LOGGER.error("Unexpected response from HSM: %s", response) + LOGGER.error("Unexpected response from HSM: %s (%s)", response, exc_type_msg(hpe)) raise HWStateManagerException(hpe) from hpe try: json_body = json.loads(response.text) @@ -77,7 +77,7 @@ def read_all_node_xnames(): return set([component['ID'] for component in json_body['Components'] if component.get('Type', None) == 'Node']) except KeyError as ke: - LOGGER.error("Unexpected API response from HSM") + LOGGER.error("Unexpected API response from HSM: %s", exc_type_msg(ke)) raise HWStateManagerException(ke) from ke @@ -121,6 +121,7 @@ def get_components(node_list, enabled=None) -> dict[str,list[dict]]: } """ if not node_list: + LOGGER.warning("hsm.get_components called with empty node list") return {'Components': []} session = requests_retry_session() try: @@ -130,17 +131,17 @@ def get_components(node_list, enabled=None) -> dict[str,list[dict]]: LOGGER.debug("POST %s with body=%s", ENDPOINT, payload) response = session.post(ENDPOINT, json=payload) LOGGER.debug("Response status code=%d, reason=%s, body=%s", response.status_code, - response.reason, response.text) + response.reason, compact_response_text(response.text)) response.raise_for_status() components = json.loads(response.text) except (ConnectionError, MaxRetryError) as e: - LOGGER.error("Unable to connect to HSM: {}".format(e)) + LOGGER.error("Unable to connect to HSM: %s", exc_type_msg(e)) raise e except HTTPError as e: - LOGGER.error("Unexpected response from HSM: {}".format(e)) + LOGGER.error("Unexpected response from HSM: %s", exc_type_msg(e)) raise e except json.JSONDecodeError as e: - LOGGER.error("Non-JSON response from HSM: {}".format(e)) + LOGGER.error("Non-JSON response from HSM: %s", exc_type_msg(e)) raise e return components @@ -226,10 +227,10 @@ def get(self, path, params=None): LOGGER.debug("HSM Inventory: GET %s with params=%s", url, params) response = self._session.get(url, params=params, verify=VERIFY) LOGGER.debug("Response status code=%d, reason=%s, body=%s", response.status_code, - response.reason, response.text) + response.reason, compact_response_text(response.text)) response.raise_for_status() except HTTPError as err: - LOGGER.error("Failed to get '{}': {}".format(url, err)) + LOGGER.error("Failed to get '%s': %s", url, exc_type_msg(err)) raise try: return response.json() diff --git a/src/bos/operators/utils/clients/pcs.py b/src/bos/operators/utils/clients/pcs.py index 21ed6f56..72ddfcf7 100644 --- a/src/bos/operators/utils/clients/pcs.py +++ b/src/bos/operators/utils/clients/pcs.py @@ -30,7 +30,7 @@ import json from collections import defaultdict -from bos.common.utils import requests_retry_session, PROTOCOL +from bos.common.utils import compact_response_text, requests_retry_session, PROTOCOL SERVICE_NAME = 'cray-power-control' POWER_CONTROL_VERSION = 'v1' @@ -103,7 +103,7 @@ def _power_status(xname=None, power_state_filter=None, management_state_filter=N LOGGER.debug("POST %s with body=%s", POWER_STATUS_ENDPOINT, params) response = session.post(POWER_STATUS_ENDPOINT, json=params) LOGGER.debug("Response status code=%d, reason=%s, body=%s", response.status_code, - response.reason, response.text) + response.reason, compact_response_text(response.text)) try: response.raise_for_status() if not response.ok: @@ -204,8 +204,8 @@ def _transition_create(xnames, operation, task_deadline_minutes=None, deputy_key session = session or requests_retry_session() try: assert operation in set(['On', 'Off', 'Soft-Off', 'Soft-Restart', 'Hard-Restart', 'Init', 'Force-Off']) - except AssertionError: - raise PowerControlSyntaxException("Operation '%s' is not supported or implemented." %(operation)) + except AssertionError as err: + raise PowerControlSyntaxException("Operation '%s' is not supported or implemented." %(operation)) from err params = {'location': [], 'operation': operation} if task_deadline_minutes: params['taskDeadlineMinutes'] = int(task_deadline_minutes) @@ -217,7 +217,7 @@ def _transition_create(xnames, operation, task_deadline_minutes=None, deputy_key LOGGER.debug("POST %s with body=%s", TRANSITION_ENDPOINT, params) response = session.post(TRANSITION_ENDPOINT, json=params) LOGGER.debug("Response status code=%d, reason=%s, body=%s", response.status_code, - response.reason, response.text) + response.reason, compact_response_text(response.text)) try: response.raise_for_status() if not response.ok: diff --git a/src/bos/operators/utils/clients/s3.py b/src/bos/operators/utils/clients/s3.py index 6535f7d1..93caa0cd 100644 --- a/src/bos/operators/utils/clients/s3.py +++ b/src/bos/operators/utils/clients/s3.py @@ -1,7 +1,7 @@ # # MIT License # -# (C) Copyright 2021-2022 Hewlett Packard Enterprise Development LP +# (C) Copyright 2021-2022, 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"), @@ -30,6 +30,8 @@ from botocore.config import Config as BotoConfig from urllib.parse import urlparse +from bos.common.utils import exc_type_msg + LOGGER = logging.getLogger('bos.operators.utils.clients.s3') @@ -110,7 +112,7 @@ def s3_client(connection_timeout=60, read_timeout=60): s3_gateway = os.environ['S3_GATEWAY'] except KeyError as error: LOGGER.error("Missing needed S3 configuration: %s", error) - raise S3MissingConfiguration(error) + raise S3MissingConfiguration(error) from error s3 = boto3.client('s3', endpoint_url=s3_protocol + "://" + s3_gateway, @@ -161,7 +163,7 @@ def object_header(self) -> dict: except ClientError as error: msg = f"s3 object {self.path} was not found." LOGGER.error(msg) - LOGGER.debug(error) + LOGGER.debug(exc_type_msg(error)) raise S3ObjectNotFound(msg) from error if self.etag and self.etag != s3_obj["ETag"].strip('\"'): @@ -194,7 +196,7 @@ def object(self): except (ClientError, ParamValidationError) as error: msg = f"Unable to download object {self.path}." LOGGER.error(msg) - LOGGER.debug(error) + LOGGER.debug(exc_type_msg(error)) raise S3ObjectNotFound(msg) from error @@ -235,7 +237,7 @@ def manifest_json(self): except Exception as error: msg = f"Unable to read manifest file '{self.path}'." LOGGER.error(msg) - LOGGER.debug(error) + LOGGER.debug(exc_type_msg(error)) raise ManifestNotFound(msg) from error # Cache the manifest.json file diff --git a/src/bos/reporter/components/state.py b/src/bos/reporter/components/state.py index 64493885..679c7743 100644 --- a/src/bos/reporter/components/state.py +++ b/src/bos/reporter/components/state.py @@ -1,7 +1,7 @@ # # MIT License # -# (C) Copyright 2021-2022 Hewlett Packard Enterprise Development LP +# (C) Copyright 2021-2022, 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"), @@ -32,6 +32,7 @@ from requests.exceptions import HTTPError, ConnectionError from urllib3.exceptions import MaxRetryError +from bos.common.utils import exc_type_msg from bos.reporter.components import BOSComponentException from bos.reporter.components import ENDPOINT as COMPONENT_ENDPOINT from bos.reporter.client import requests_retry_session @@ -61,8 +62,8 @@ def patch_component(component, properties, session=None): try: response = session.patch(component_endpoint, json=properties) except (ConnectionError, MaxRetryError) as ce: - LOGGER.warning("Could not connect to BOS API service: %s" % (ce)) - raise BOSComponentException(ce) + LOGGER.warning("Could not connect to BOS API service: %s", exc_type_msg(ce)) + raise BOSComponentException(ce) from ce try: response.raise_for_status() except HTTPError as hpe: @@ -71,9 +72,9 @@ def patch_component(component, properties, session=None): json_response = json.loads(response.text) raise UnknownComponent(json_response['detail']) except json.JSONDecodeError as jde: - raise UnrecognizedResponse("BOS returned a non-json response: %s\n%s" % (response.text, jde)) + raise UnrecognizedResponse("BOS returned a non-json response: %s\n%s" % (response.text, jde)) from jde LOGGER.warning("Unexpected response from '%s':\n%s: %s", component_endpoint, response.status_code, response.text) - raise BOSComponentException(hpe) + raise BOSComponentException(hpe) from hpe def report_state(component, state, session=None): @@ -82,4 +83,3 @@ def report_state(component, state, session=None): """ data = {'id': component, 'actual_state': state} patch_component(component, data, session=session) - diff --git a/src/bos/reporter/node_identity.py b/src/bos/reporter/node_identity.py index 80386819..85934e98 100644 --- a/src/bos/reporter/node_identity.py +++ b/src/bos/reporter/node_identity.py @@ -1,7 +1,7 @@ # # MIT License # -# (C) Copyright 2021-2022 Hewlett Packard Enterprise Development LP +# (C) Copyright 2021-2022, 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"), @@ -60,8 +60,8 @@ def identity_from_environment(): ident_string = 'NODE_IDENTITY' try: return os.environ[ident_string] - except KeyError: - raise UnknownIdentity("Node identity not passed in via environment '%s'" % (ident_string)) + except KeyError as exc: + raise UnknownIdentity("Node identity not passed in via environment '%s'" % (ident_string)) from exc def read_identity(): diff --git a/src/bos/reporter/status_reporter/__main__.py b/src/bos/reporter/status_reporter/__main__.py index 10a1fec0..efd6a512 100644 --- a/src/bos/reporter/status_reporter/__main__.py +++ b/src/bos/reporter/status_reporter/__main__.py @@ -1,7 +1,7 @@ # # MIT License # -# (C) Copyright 2020-2022 Hewlett Packard Enterprise Development LP +# (C) Copyright 2020-2022, 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"), @@ -28,6 +28,7 @@ import datetime from time import sleep +from bos.common.utils import exc_type_msg from bos.reporter.client import requests_retry_session from bos.reporter.node_identity import read_identity from bos.reporter.components.state import report_state, BOSComponentException, UnknownComponent @@ -86,7 +87,7 @@ def report_state_until_success(component): LOGGER.warning("Unable to contact BOS to report component status: %s" % (cce)) continue except OSError as exc: - LOGGER.error("BOS client encountered an %s" % (exc)) + LOGGER.error("BOS client encountered an error: %s", exc_type_msg(exc)) continue LOGGER.info("Updated the actual_state record for BOS component '%s'." % (component)) return @@ -133,7 +134,7 @@ def main(): try: report_state_until_success(component) except Exception as exp: - LOGGER.error("An error occurred: {}".format(exp)) + LOGGER.error("An error occurred: %s", exc_type_msg(exp)) if has_slept_before: sleep(sleep_time) else: diff --git a/src/bos/server/controllers/v2/base.py b/src/bos/server/controllers/v2/base.py index a66dfbe5..620e9a71 100644 --- a/src/bos/server/controllers/v2/base.py +++ b/src/bos/server/controllers/v2/base.py @@ -26,6 +26,7 @@ import subprocess import yaml +from bos.common.utils import exc_type_msg from bos.server.controllers.utils import url_for from bos.server.models import Version, Link from os import path @@ -55,7 +56,7 @@ def calc_version(details): try: f = open(openapispec_f, 'r') except IOError as e: - LOGGER.debug('error opening openapi.yaml file: %s' % e) + LOGGER.debug('error opening "%s" file: %s', openapispec_f, exc_type_msg(e)) openapispec_map = yaml.safe_load(f) f.close() diff --git a/src/bos/server/controllers/v2/boot_set.py b/src/bos/server/controllers/v2/boot_set.py index 5f061ec9..0c1d4b25 100644 --- a/src/bos/server/controllers/v2/boot_set.py +++ b/src/bos/server/controllers/v2/boot_set.py @@ -1,7 +1,7 @@ # # MIT License # -# (C) Copyright 2021-2022 Hewlett Packard Enterprise Development LP +# (C) Copyright 2021-2022, 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"), @@ -23,6 +23,7 @@ # import logging +from bos.common.utils import exc_type_msg from bos.operators.utils.boot_image_metadata.factory import BootImageMetaDataFactory from bos.operators.utils.clients.s3 import S3Object, ArtifactNotFound @@ -81,7 +82,7 @@ def validate_boot_sets(session_template: dict, image_metadata = BootImageMetaDataFactory(bs)() except Exception as err: msg = f"Session template: '{template_name}' boot set: '{bs_name}' " \ - f"could not locate its boot artifacts. Error: {err}" + f"could not locate its boot artifacts. Error: " + exc_type_msg(err) LOGGER.error(msg) return BOOT_SET_ERROR, msg @@ -95,7 +96,7 @@ def validate_boot_sets(session_template: dict, _ = obj.object_header except Exception as err: msg = f"Session template: '{template_name}' boot set: '{bs_name}' " \ - f"could not locate its {boot_artifact}. Error: {err}" + f"could not locate its {boot_artifact}. Error: " + exc_type_msg(err) LOGGER.error(msg) return BOOT_SET_ERROR, msg @@ -113,7 +114,7 @@ def validate_boot_sets(session_template: dict, _ = obj.object_header except Exception as err: msg = f"Session template: '{template_name}' boot set: '{bs_name}' " \ - f"could not locate its {boot_artifact}. Warning: {err}" + f"could not locate its {boot_artifact}. Warning: " + exc_type_msg(err) LOGGER.warn(msg) warning_flag = True warn_msg = warn_msg + msg diff --git a/src/bos/server/controllers/v2/components.py b/src/bos/server/controllers/v2/components.py index b212ceec..e32b0499 100644 --- a/src/bos/server/controllers/v2/components.py +++ b/src/bos/server/controllers/v2/components.py @@ -24,7 +24,7 @@ import connexion import logging -from bos.common.utils import get_current_timestamp +from bos.common.utils import exc_type_msg, get_current_timestamp from bos.common.tenant_utils import get_tenant_from_header, get_tenant_component_set, tenant_error_handler from bos.common.values import Phase, Action, Status, EMPTY_STAGED_STATE, EMPTY_BOOT_ARTIFACTS from bos.server import redis_db_utils as dbutils @@ -54,6 +54,7 @@ def get_v2_components(ids="", enabled=None, session=None, staged_session=None, p try: id_list = ids.split(',') except Exception as err: + LOGGER.error("Error parsing component IDs: %s", exc_type_msg(err)) return connexion.problem( status=400, title="Error parsing the ids provided.", detail=str(err)) @@ -179,7 +180,7 @@ def put_v2_components(): ComponentArray.from_dict(data) # noqa: E501 except Exception as err: msg="Provided data does not follow API spec" - LOGGER.exception(msg) + LOGGER.error("%s: %s", msg, exc_type_msg(err)) return connexion.problem(status=400, title=msg,detail=str(err)) components = [] @@ -220,7 +221,7 @@ def patch_v2_components(): ComponentArray.from_dict(data) # noqa: E501 except Exception as err: msg="Provided data does not follow API spec" - LOGGER.exception(msg) + LOGGER.error("%s: %s", msg, exc_type_msg(err)) return connexion.problem(status=400, title=msg,detail=str(err)) return patch_v2_components_list(data) elif type(data) == dict: @@ -230,13 +231,14 @@ def patch_v2_components(): ComponentsUpdate.from_dict(data) # noqa: E501 except Exception as err: msg="Provided data does not follow API spec" - LOGGER.exception(msg) + LOGGER.error("%s: %s", msg, exc_type_msg(err)) return connexion.problem(status=400, title=msg,detail=str(err)) return patch_v2_components_dict(data) + LOGGER.error("Unexpected data type %s", str(type(data))) return connexion.problem( - status=400, title="Error parsing the data provided.", - detail="Unexpected data type {}".format(str(type(data)))) + status=400, title="Error parsing the data provided.", + detail="Unexpected data type {}".format(str(type(data)))) def patch_v2_components_list(data): @@ -246,11 +248,13 @@ def patch_v2_components_list(data): for component_data in data: component_id = component_data['id'] if component_id not in DB or not _is_valid_tenant_component(component_id): + LOGGER.warning("Component %s could not be found", component_id) return connexion.problem( status=404, title="Component not found.", detail="Component {} could not be found".format(component_id)) components.append((component_id, component_data)) except Exception as err: + LOGGER.error("Error loading component data: %s", exc_type_msg(err)) return connexion.problem( status=400, title="Error parsing the data provided.", detail=str(err)) @@ -268,6 +272,7 @@ def patch_v2_components_dict(data): ids = filters.get("ids", None) session = filters.get("session", None) if ids and session: + LOGGER.warning("Multiple filters provided") return connexion.problem( status=400, title="Only one filter may be provided.", detail="Only one filter may be provided.") @@ -275,6 +280,7 @@ def patch_v2_components_dict(data): try: id_list = ids.split(',') except Exception as err: + LOGGER.error("Error parsing the IDs provided: %s", exc_type_msg(err)) return connexion.problem( status=400, title="Error parsing the ids provided.", detail=str(err)) @@ -289,6 +295,7 @@ def patch_v2_components_dict(data): id_list = [component["id"] for component in get_v2_components_data(session=session, tenant=get_tenant_from_header())] LOGGER.debug("patch_v2_components_dict: %d IDs found for specified session", len(id_list)) else: + LOGGER.warning("No filter provided") return connexion.problem( status=400, title="Exactly one filter must be provided.", detail="Exactly one filter may be provided.") @@ -308,6 +315,7 @@ def get_v2_component(component_id): """Used by the GET /components/{component_id} API operation""" LOGGER.debug("GET /v2/components/%s invoked get_v2_component", component_id) if component_id not in DB or not _is_valid_tenant_component(component_id): + LOGGER.warning("Component %s could not be found", component_id) return connexion.problem( status=404, title="Component not found.", detail="Component {} could not be found".format(component_id)) @@ -337,7 +345,7 @@ def put_v2_component(component_id): Component.from_dict(data) # noqa: E501 except Exception as err: msg="Provided data does not follow API spec" - LOGGER.exception(msg) + LOGGER.error("%s: %s", msg, exc_type_msg(err)) return connexion.problem(status=400, title=msg,detail=str(err)) data['id'] = component_id data = _set_auto_fields(data) @@ -365,14 +373,16 @@ def patch_v2_component(component_id): Component.from_dict(data) # noqa: E501 except Exception as err: msg="Provided data does not follow API spec" - LOGGER.exception(msg) + LOGGER.error("%s: %s", msg, exc_type_msg(err)) return connexion.problem(status=400, title=msg,detail=str(err)) if component_id not in DB or not _is_valid_tenant_component(component_id): + LOGGER.warning("Component %s could not be found", component_id) return connexion.problem( status=404, title="Component not found.", detail="Component {} could not be found".format(component_id)) if "actual_state" in data and not validate_actual_state_change_is_allowed(component_id): + LOGGER.warning("Not able to update actual state") return connexion.problem( status=409, title="Actual state can not be updated.", detail="BOS is currently changing the state of the node," @@ -407,6 +417,7 @@ def delete_v2_component(component_id): """Used by the DELETE /components/{component_id} API operation""" LOGGER.debug("DELETE /v2/components/%s invoked delete_v2_component", component_id) if component_id not in DB or not _is_valid_tenant_component(component_id): + LOGGER.warning("Component %s could not be found", component_id) return connexion.problem( status=404, title="Component not found.", detail="Component {} could not be found".format(component_id)) @@ -433,10 +444,11 @@ def post_v2_apply_staged(): response["succeeded"].append(xname) else: response["ignored"].append(xname) - except Exception as err: - LOGGER.error(f"An error was encountered while attempting to apply stage for node {xname}: {err}") + except Exception: + LOGGER.exception("An error was encountered while attempting to apply stage for node %s", xname) response["failed"].append(xname) except Exception as err: + LOGGER.error("Error parsing request data: %s", exc_type_msg(err)) return connexion.problem( status=400, title="Error parsing the data provided.", detail=str(err)) diff --git a/src/bos/server/controllers/v2/healthz.py b/src/bos/server/controllers/v2/healthz.py index cdfc5912..797eadbc 100644 --- a/src/bos/server/controllers/v2/healthz.py +++ b/src/bos/server/controllers/v2/healthz.py @@ -23,6 +23,7 @@ # import logging +from bos.common.utils import exc_type_msg from bos.server.models.healthz import Healthz as Healthz from bos.server import redis_db_utils @@ -37,7 +38,7 @@ def _get_db_status(): if DB.info(): available = True except Exception as e: - LOGGER.error(e) + LOGGER.error(exc_type_msg(e)) if available: return 'ok' diff --git a/src/bos/server/controllers/v2/options.py b/src/bos/server/controllers/v2/options.py index 85ae54d6..cdf78303 100644 --- a/src/bos/server/controllers/v2/options.py +++ b/src/bos/server/controllers/v2/options.py @@ -26,6 +26,7 @@ import threading import time +from bos.common.utils import exc_type_msg from bos.server import redis_db_utils as dbutils from bos.server.models.v2_options import V2Options as Options @@ -60,8 +61,8 @@ def _init(): try: data = DB.get(OPTIONS_KEY) break - except Exception: - LOGGER.info('Database is not yet available') + except Exception as err: + LOGGER.info('Database is not yet available (%s)', exc_type_msg(err)) time.sleep(1) if not data: return @@ -116,6 +117,7 @@ def patch_v2_options(): try: data = connexion.request.get_json() except Exception as err: + LOGGER.error("Error parsing request data: %s", exc_type_msg(err)) return connexion.problem( status=400, title="Error parsing the data provided.", detail=str(err)) @@ -142,6 +144,6 @@ def check_v2_logging_level(): data = get_v2_options_data() if 'logging_level' in data: update_log_level(data['logging_level']) - except Exception as e: - LOGGER.debug(e) + except Exception as err: + LOGGER.debug("Error checking or updating log level: %s", exc_type_msg(err)) time.sleep(5) diff --git a/src/bos/server/controllers/v2/sessions.py b/src/bos/server/controllers/v2/sessions.py index daef9c0a..dda45e0d 100644 --- a/src/bos/server/controllers/v2/sessions.py +++ b/src/bos/server/controllers/v2/sessions.py @@ -30,7 +30,7 @@ from connexion.lifecycle import ConnexionResponse from bos.common.tenant_utils import get_tenant_from_header, get_tenant_aware_key, reject_invalid_tenant -from bos.common.utils import get_current_time, get_current_timestamp, load_timestamp +from bos.common.utils import exc_type_msg, get_current_time, get_current_timestamp, load_timestamp from bos.common.values import Phase, Status from bos.server import redis_db_utils as dbutils from bos.server.controllers.v2.components import get_v2_components_data @@ -83,6 +83,7 @@ def post_v2_session(): # noqa: E501 # Validate health/validity of the sessiontemplate before creating a session error_code, msg = validate_boot_sets(session_template, session_create.operation, template_name) if error_code >= BOOT_SET_ERROR: + LOGGER.error("Session template fails check: %s", msg) return msg, 400 # -- Setup Record -- @@ -90,6 +91,7 @@ def post_v2_session(): # noqa: E501 session = _create_session(session_create, tenant) session_key = get_tenant_aware_key(session.name, tenant) if session_key in DB: + LOGGER.warning("v2 session named %s already exists", session.name) return connexion.problem( detail="A session with the name {} already exists".format(session.name), status=409, @@ -146,6 +148,7 @@ def patch_v2_session(session_id): session_key = get_tenant_aware_key(session_id, get_tenant_from_header()) if session_key not in DB: + LOGGER.warning("Could not find v2 session %s", session_id) return connexion.problem( status=404, title="Session could not found.", detail="Session {} could not be found".format(session_id)) @@ -166,6 +169,7 @@ def get_v2_session(session_id): # noqa: E501 LOGGER.debug("GET /v2/sessions/%s invoked get_v2_session", session_id) session_key = get_tenant_aware_key(session_id, get_tenant_from_header()) if session_key not in DB: + LOGGER.warning("Could not find v2 session %s", session_id) return connexion.problem( status=404, title="Session could not found.", detail="Session {} could not be found".format(session_id)) @@ -197,6 +201,7 @@ def delete_v2_session(session_id): # noqa: E501 LOGGER.debug("DELETE /v2/sessions/%s invoked delete_v2_session", session_id) session_key = get_tenant_aware_key(session_id, get_tenant_from_header()) if session_key not in DB: + LOGGER.warning("Could not find v2 session %s", session_id) return connexion.problem( status=404, title="Session could not found.", detail="Session {} could not be found".format(session_id)) @@ -214,6 +219,7 @@ def delete_v2_sessions(min_age=None, max_age=None, status=None): # noqa: E501 sessions = _get_filtered_sessions(tenant=tenant, min_age=min_age, max_age=max_age, status=status) except ParsingException as err: + LOGGER.error("Error parsing age field: %s", exc_type_msg(err)) return connexion.problem( detail=str(err), status=400, @@ -241,6 +247,7 @@ def get_v2_session_status(session_id): # noqa: E501 LOGGER.debug("GET /v2/sessions/status/%s invoked get_v2_session_status", session_id) session_key = get_tenant_aware_key(session_id, get_tenant_from_header()) if session_key not in DB: + LOGGER.warning("Could not find v2 session %s", session_id) return connexion.problem( status=404, title="Session could not found.", detail="Session {} could not be found".format(session_id)) @@ -263,6 +270,7 @@ def save_v2_session_status(session_id): # noqa: E501 LOGGER.debug("POST /v2/sessions/status/%s invoked save_v2_session_status", session_id) session_key = get_tenant_aware_key(session_id, get_tenant_from_header()) if session_key not in DB: + LOGGER.warning("Could not find v2 session %s", session_id) return connexion.problem( status=404, title="Session could not found.", detail="Session {} could not be found".format(session_id)) diff --git a/src/bos/server/controllers/v2/sessiontemplates.py b/src/bos/server/controllers/v2/sessiontemplates.py index 9b21abe9..6332969e 100644 --- a/src/bos/server/controllers/v2/sessiontemplates.py +++ b/src/bos/server/controllers/v2/sessiontemplates.py @@ -25,6 +25,7 @@ import connexion from bos.common.tenant_utils import get_tenant_from_header, get_tenant_aware_key, reject_invalid_tenant +from bos.common.utils import exc_type_msg from bos.server.models.v2_session_template import V2SessionTemplate as SessionTemplate # noqa: E501 from bos.server import redis_db_utils as dbutils from bos.server.utils import _canonize_xname @@ -92,6 +93,7 @@ def put_v2_sessiontemplate(session_template_id): # noqa: E501 try: data = connexion.request.get_json() except Exception as err: + LOGGER.error("Error parsing request data: %s", exc_type_msg(err)) return connexion.problem( status=400, title="Error parsing the data provided.", detail=str(err)) @@ -108,6 +110,7 @@ def put_v2_sessiontemplate(session_template_id): # noqa: E501 """ SessionTemplate.from_dict(template_data) except Exception as err: + LOGGER.error("Error creating session template: %s", exc_type_msg(err)) return connexion.problem( status=400, title="The session template could not be created.", detail=str(err)) @@ -143,6 +146,7 @@ def get_v2_sessiontemplate(session_template_id): LOGGER.debug("GET /v2/sessiontemplates/%s invoked get_v2_sessiontemplate", session_template_id) template_key = get_tenant_aware_key(session_template_id, get_tenant_from_header()) if template_key not in DB: + LOGGER.warning("Session template not found: %s", session_template_id) return connexion.problem( status=404, title="Sessiontemplate could not found.", detail="Sessiontemplate {} could not be found".format(session_template_id)) @@ -171,6 +175,7 @@ def delete_v2_sessiontemplate(session_template_id): LOGGER.debug("DELETE /v2/sessiontemplates/%s invoked delete_v2_sessiontemplate", session_template_id) template_key = get_tenant_aware_key(session_template_id, get_tenant_from_header()) if template_key not in DB: + LOGGER.warning("Session template not found: %s", session_template_id) return connexion.problem( status=404, title="Sessiontemplate could not found.", detail="Sessiontemplate {} could not be found".format(session_template_id)) @@ -187,6 +192,7 @@ def patch_v2_sessiontemplate(session_template_id): LOGGER.debug("PATCH /v2/sessiontemplates/%s invoked patch_v2_sessiontemplate", session_template_id) template_key = get_tenant_aware_key(session_template_id, get_tenant_from_header()) if template_key not in DB: + LOGGER.warning("Session template not found: %s", session_template_id) return connexion.problem( status=404, title="Sessiontemplate could not found.", detail="Sessiontemplate {} could not be found".format(session_template_id)) @@ -201,6 +207,7 @@ def patch_v2_sessiontemplate(session_template_id): try: data = connexion.request.get_json() except Exception as err: + LOGGER.error("Error parsing request data: %s", exc_type_msg(err)) return connexion.problem( status=400, title="Error parsing the data provided.", detail=str(err)) @@ -217,8 +224,9 @@ def patch_v2_sessiontemplate(session_template_id): """ SessionTemplate.from_dict(template_data) except Exception as err: + LOGGER.error("Error patching session template: %s", exc_type_msg(err)) return connexion.problem( - status=400, title="The session template could not be created.", + status=400, title="The session template could not be patched.", detail=str(err)) template_data = _sanitize_xnames(template_data) diff --git a/src/bos/server/redis_db_utils.py b/src/bos/server/redis_db_utils.py index e38c84a5..faf906bc 100644 --- a/src/bos/server/redis_db_utils.py +++ b/src/bos/server/redis_db_utils.py @@ -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"), @@ -27,6 +27,8 @@ import logging import redis +from bos.common.utils import exc_type_msg + LOGGER = logging.getLogger(__name__) DATABASES = ["options", "components", "session_templates", "sessions", "bss_tokens_boot_artifacts", "session_status"] # Index is the db id. @@ -67,7 +69,7 @@ def _get_client(self, db_id): return redis.Redis(host=DB_HOST, port=DB_PORT, db=db_id) except Exception as err: LOGGER.error("Failed to connect to database %s : %s", - db_id, err) + db_id, exc_type_msg(err)) raise # The following methods act like REST calls for single items diff --git a/update_external_versions.conf b/update_external_versions.conf index fa740387..8a7520f8 100644 --- a/update_external_versions.conf +++ b/update_external_versions.conf @@ -78,10 +78,6 @@ # in the file paths (like on arti.dev), the type parameter should be omitted entirely, otherwise # no images will be found. -image: cray-boa - major: 1 - minor: 4 - # Built from the k8s-liveness repository image: liveness source: python