Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CASMCMS-8952: Improve handling of no-op situations #283

Merged
merged 5 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Loading