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 3 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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ 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.

### 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
27 changes: 25 additions & 2 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 @@ -118,9 +118,12 @@ 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
# what the powerState field suggests. This is a major departure from how CAPMC handled errors.
Expand All @@ -139,12 +142,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 Down Expand Up @@ -202,6 +209,10 @@ 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:
# Should probably raise an exception here, since we don't want to actually call PCS
# with an empty node list. Suggestions welcome for what to raise.
LOGGER.error("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 +221,10 @@ 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:
# Should probably raise an exception here, since we don't want to actually call PCS
# with an empty node list. Suggestions welcome for what to raise.
LOGGER.error("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 +233,10 @@ 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:
# Should probably raise an exception here, since we don't want to actually call PCS
# with an empty node list. Suggestions welcome for what to raise.
LOGGER.error("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 +245,10 @@ 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:
# Should probably raise an exception here, since we don't want to actually call PCS
# with an empty node list. Suggestions welcome for what to raise.
LOGGER.error("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