Skip to content

Commit

Permalink
Merge pull request #283 from Cray-HPE/casmcms-8952-csm-1.6
Browse files Browse the repository at this point in the history
CASMCMS-8952: Improve handling of no-op situations
  • Loading branch information
mharding-hpe authored Mar 27, 2024
2 parents 0859cdd + a6223f8 commit bb880d4
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 8 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Add code to the beginning of some CFS functions to check if they have been called without
necessary arguments, and if so, to log a warning and return immediately.
- Added similar code to some PCS functions.
- Created `PowerControlComponentsEmptyException`; raise it when some PCS functions receive
empty component list arguments.

### Changed
- If the status operator `_run` method finds no enabled components, stop immediately, as there is
nothing to do.

## [2.17.1] - 2024-03-21
### Changed
Expand Down
12 changes: 7 additions & 5 deletions src/bos/operators/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# MIT License
#
# (C) Copyright 2022-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"),
Expand Down Expand Up @@ -69,14 +69,16 @@ def _act(self, components):
def _run(self) -> None:
""" A single pass of detecting and acting on components """
components = self.bos_client.components.get_components(enabled=True)
if not components:
LOGGER.debug('No enabled components found')
return
component_ids = [component['id'] for component in components]
power_states = node_to_powerstate(component_ids)
cfs_states = self._get_cfs_components()
updated_components = []
if components:
# Recreate these filters to pull in the latest options values
self.boot_wait_time_elapsed = TimeSinceLastAction(seconds=options.max_boot_wait_time)._match
self.power_on_wait_time_elapsed = TimeSinceLastAction(seconds=options.max_power_on_wait_time)._match
# Recreate these filters to pull in the latest options values
self.boot_wait_time_elapsed = TimeSinceLastAction(seconds=options.max_boot_wait_time)._match
self.power_on_wait_time_elapsed = TimeSinceLastAction(seconds=options.max_power_on_wait_time)._match
for component in components:
updated_component = self._check_status(
component, power_states.get(component['id']), cfs_states.get(component['id']))
Expand Down
12 changes: 12 additions & 0 deletions src/bos/operators/utils/clients/cfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ def get_components(session=None, **kwargs):


def patch_components(data, session=None):
if not data:
LOGGER.warning("patch_components called without data; returning without action.")
return
if not session:
session = requests_retry_session()
LOGGER.debug("PATCH %s with body=%s", COMPONENTS_ENDPOINT, data)
Expand All @@ -69,6 +72,9 @@ def patch_components(data, session=None):


def get_components_from_id_list(id_list):
if not id_list:
LOGGER.warning("get_components_from_id_list called without IDs; returning without action.")
return []
LOGGER.debug("get_components_from_id_list called with %d IDs", len(id_list))
session = requests_retry_session()
component_list = []
Expand All @@ -83,6 +89,9 @@ def get_components_from_id_list(id_list):


def patch_desired_config(node_ids, desired_config, enabled=False, tags=None, clear_state=False):
if not node_ids:
LOGGER.warning("patch_desired_config called without IDs; returning without action.")
return
LOGGER.debug("patch_desired_config called on %d IDs with desired_config=%s enabled=%s tags=%s"
" clear_state=%s", len(node_ids), desired_config, enabled, tags, clear_state)
session = requests_retry_session()
Expand All @@ -107,6 +116,9 @@ def patch_desired_config(node_ids, desired_config, enabled=False, tags=None, cle


def set_cfs(components, enabled, clear_state=False):
if not components:
LOGGER.warning("set_cfs called without components; returning without action.")
return
LOGGER.debug("set_cfs called on %d components with enabled=%s clear_state=%s", len(components),
enabled, clear_state)
configurations = defaultdict(list)
Expand Down
39 changes: 36 additions & 3 deletions src/bos/operators/utils/clients/pcs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# MIT License
#
# (C) Copyright 2023 Hewlett Packard Enterprise Development LP
# (C) Copyright 2023-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 @@ -58,6 +58,20 @@ class PowerControlTimeoutException(PowerControlException):
"""


class PowerControlComponentsEmptyException(Exception):
"""
Raised when one of the PCS utility functions that requires a non-empty
list of components is passed an empty component list. This will only
happen in the case of a programming bug.
This exception is not raised for functions that require a node list
but that are able to return a sensible object to the caller that
indicates nothing has been done. For example, the status function.
This exception is instead used for functions that will fail if they run
with an empty node list, but which cannot return an appropriate
"no-op" value to the caller.
"""

def _power_status(xname=None, power_state_filter=None, management_state_filter=None,
session=None):
"""
Expand Down Expand Up @@ -118,11 +132,14 @@ def status(nodes, session=None, **kwargs):
PowerControlException: Any non-nominal response from PCS.
JSONDecodeError: Error decoding the PCS response
"""
status_bucket = defaultdict(set)
if not nodes:
LOGGER.warning("status called without nodes; returning without action.")
return status_bucket
session = session or requests_retry_session()
power_status_all = _power_status(xname=list(nodes), session=session, **kwargs)
status_bucket = defaultdict(set)
for power_status_entry in power_status_all['status']:
# IF the returned xname has an error, it itself is the status regardless of
# If the returned xname has an error, it itself is the status regardless of
# what the powerState field suggests. This is a major departure from how CAPMC handled errors.
xname = power_status_entry.get('xname', '')
if power_status_entry['error']:
Expand All @@ -139,12 +156,16 @@ def node_to_powerstate(nodes, session=None, **kwargs):
For an iterable of nodes <nodes>; return a dictionary that maps to the current power state for the node in question.
"""
power_states = {}
if not nodes:
LOGGER.warning("node_to_powerstate called without nodes; returning without action.")
return power_states
session = session or requests_retry_session()
status_bucket = status(nodes, session, **kwargs)
for pstatus, nodeset in status_bucket.items():
for node in nodeset:
power_states[node] = pstatus
return power_states

def _transition_create(xnames, operation, task_deadline_minutes=None, deputy_key=None, session=None):
"""
Interact with PCS to create a request to transition one or more xnames. The transition
Expand All @@ -170,7 +191,11 @@ def _transition_create(xnames, operation, task_deadline_minutes=None, deputy_key
Raises:
PowerControlException: Any non-nominal response from PCS, typically as a result of an unexpected payload
response, or a failure to create a transition record.
PowerControlComponentsEmptyException: No xnames specified
"""
if not xnames:
raise PowerControlComponentsEmptyException(
"_transition_create called with no xnames! (operation=%s)" % operation)
session = session or requests_retry_session()
try:
assert operation in set(['On', 'Off', 'Soft-Off', 'Soft-Restart', 'Hard-Restart', 'Init', 'Force-Off'])
Expand Down Expand Up @@ -202,6 +227,8 @@ def power_on(nodes, session=None, task_deadline_minutes=1, **kwargs):
Sends a request to PCS for transitioning nodes in question to a powered on state.
Returns: A JSON parsed object response from PCS, which includes the created request ID.
"""
if not nodes:
raise PowerControlComponentsEmptyException("power_on called with no nodes!")
session = session or requests_retry_session()
return _transition_create(xnames=nodes, operation='On', task_deadline_minutes=task_deadline_minutes,
session=session, **kwargs)
Expand All @@ -210,6 +237,8 @@ def power_off(nodes, session=None, task_deadline_minutes=1, **kwargs):
Sends a request to PCS for transitioning nodes in question to a powered off state (graceful).
Returns: A JSON parsed object response from PCS, which includes the created request ID.
"""
if not nodes:
raise PowerControlComponentsEmptyException("power_off called with no nodes!")
session = session or requests_retry_session()
return _transition_create(xnames=nodes, operation='Off', task_deadline_minutes=task_deadline_minutes,
session=session, **kwargs)
Expand All @@ -218,6 +247,8 @@ def soft_off(nodes, session=None, task_deadline_minutes=1, **kwargs):
Sends a request to PCS for transitioning nodes in question to a powered off state (graceful).
Returns: A JSON parsed object response from PCS, which includes the created request ID.
"""
if not nodes:
raise PowerControlComponentsEmptyException("soft_off called with no nodes!")
session = session or requests_retry_session()
return _transition_create(xnames=nodes, operation='Soft-Off', task_deadline_minutes=task_deadline_minutes,
session=session, **kwargs)
Expand All @@ -226,6 +257,8 @@ def force_off(nodes, session=None, task_deadline_minutes=1, **kwargs):
Sends a request to PCS for transitioning nodes in question to a powered off state (forceful).
Returns: A JSON parsed object response from PCS, which includes the created request ID.
"""
if not nodes:
raise PowerControlComponentsEmptyException("force_off called with no nodes!")
session = session or requests_retry_session()
return _transition_create(xnames=nodes, operation='Force-Off', task_deadline_minutes=task_deadline_minutes,
session=session, **kwargs)

0 comments on commit bb880d4

Please sign in to comment.