Skip to content

Commit

Permalink
CASMCMS-9225: Move CFS client to new paradigm
Browse files Browse the repository at this point in the history
  • Loading branch information
mharding-hpe committed Dec 17, 2024
1 parent 92b42a8 commit e340171
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 21 deletions.
24 changes: 24 additions & 0 deletions src/bos/common/clients/cfs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#
# MIT License
#
# (C) Copyright 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"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
from .client import CFSClient
69 changes: 69 additions & 0 deletions src/bos/common/clients/cfs/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#
# MIT License
#
# (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"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
from abc import ABC
import logging

from bos.common.clients.endpoints import BaseEndpoint
from bos.common.utils import PROTOCOL

LOGGER = logging.getLogger(__name__)

SERVICE_NAME = 'cray-cfs-api'
BASE_CFS_ENDPOINT = f"{PROTOCOL}://{SERVICE_NAME}/v3"


class BaseCfsEndpoint(BaseEndpoint, ABC):
"""
This base class provides generic access to the CFS API.
"""
BASE_ENDPOINT = BASE_CFS_ENDPOINT

def get_items(self, **kwargs):
"""Get information for all CFS items"""
return self.get(params=kwargs)

def update_items(self, data):
"""Update information for multiple CFS items"""
return self.patch(json=data)


class BasePagedCfsEndpoint(BaseCfsEndpoint, ABC):
"""
This base class provides generic access to the CFS API, for endpoints that support paging.
"""
ITEM_FIELD_NAME = ''

def get_items(self, **kwargs):
"""Get information for all CFS items"""
item_list = []
while kwargs is not None:
response_json = super().get_items(**kwargs)
new_items = response_json[self.ITEM_FIELD_NAME]
LOGGER.debug("Query returned %d %ss", len(new_items),
self.ITEM_FIELD_NAME)
item_list.extend(new_items)
kwargs = response_json["next"]
LOGGER.debug("Returning %d %ss from CFS", len(item_list),
self.ITEM_FIELD_NAME)
return item_list
37 changes: 37 additions & 0 deletions src/bos/common/clients/cfs/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#
# MIT License
#
# (C) Copyright 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"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
from bos.common.clients.api_client import APIClient
from bos.operators.utils.clients.bos.options import options

from .components import ComponentEndpoint


class CFSClient(APIClient):

def __init__(self):
super().__init__(read_timeout=options.cfs_read_timeout)

@property
def components(self) -> ComponentEndpoint:
return self.get_endpoint(ComponentEndpoint)
113 changes: 113 additions & 0 deletions src/bos/common/clients/cfs/components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#
# MIT License
#
# (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"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
from collections import defaultdict
import logging

from .base import BasePagedCfsEndpoint

LOGGER = logging.getLogger(__name__)

GET_BATCH_SIZE = 200
PATCH_BATCH_SIZE = 1000


class ComponentEndpoint(BasePagedCfsEndpoint):
ENDPOINT = 'components'
ITEM_FIELD_NAME = 'components'

def get_components(self, **kwargs):
return self.get_items(**kwargs)

def patch_components(self, data):
return self.update_items(data)

def get_components_from_id_list(self, 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))
component_list = []
while id_list:
next_batch = id_list[:GET_BATCH_SIZE]
next_comps = self.get_components(ids=','.join(next_batch))
component_list.extend(next_comps)
id_list = id_list[GET_BATCH_SIZE:]
LOGGER.debug(
"get_components_from_id_list returning a total of %d components from CFS",
len(component_list))
return component_list

def patch_desired_config(self,
node_ids,
desired_config,
enabled: bool = False,
tags=None,
clear_state: bool = 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)
node_patch = {
'enabled': enabled,
'desired_config': desired_config,
'tags': tags if tags else {}
}
data = {"patch": node_patch, "filters": {}}
if clear_state:
node_patch['state'] = []
while node_ids:
data["filters"]["ids"] = ','.join(node_ids[:PATCH_BATCH_SIZE])
self.patch_components(data)
node_ids = node_ids[PATCH_BATCH_SIZE:]

def set_cfs(self, components, enabled: bool, clear_state: bool = 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)
for component in components:
config_name = component.get('desired_state',
{}).get('configuration', '')
bos_session = component.get('session')
key = (config_name, bos_session)
configurations[key].append(component['id'])
for key, ids in configurations.items():
config_name, bos_session = key
self.patch_desired_config(ids,
config_name,
enabled=enabled,
tags={'bos_session': bos_session},
clear_state=clear_state)
15 changes: 12 additions & 3 deletions src/bos/operators/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@
from typing import Generator, List, NoReturn, Type

from bos.common.clients.bss import BSSClient
from bos.common.clients.cfs import CFSClient
from bos.common.clients.pcs import PCSClient
from bos.common.utils import exc_type_msg
from bos.common.values import Status
from bos.operators.filters import DesiredConfigurationSetInCFS
from bos.operators.filters.base import BaseFilter
from bos.operators.utils.clients.bos.options import options
from bos.operators.utils.clients.bos import BOSClient
Expand Down Expand Up @@ -67,7 +69,7 @@ class ApiClients:
def __init__(self):
#self.bos = BOSClient()
self.bss = BSSClient()
#self.cfs = CFSClient()
self.cfs = CFSClient()
#self.hsm = HSMClient()
#self.ims = IMSClient()
self.pcs = PCSClient()
Expand All @@ -79,7 +81,7 @@ def __enter__(self):
"""
#self._stack.enter_context(self.bos)
self._stack.enter_context(self.bss)
#self._stack.enter_context(self.cfs)
self._stack.enter_context(self.cfs)
#self._stack.enter_context(self.hsm)
#self._stack.enter_context(self.ims)
self._stack.enter_context(self.pcs)
Expand Down Expand Up @@ -120,7 +122,7 @@ def __init__(self) -> NoReturn:
def client(self) -> ApiClients:
"""
Return the ApiClients object for this operator.
If it is not initialized, raise a ValueError (this should never be the case).
If it is not initialized, raise a ValueError (this should never be the case).
"""
if self._client is None:
raise ValueError("Attempted to access uninitialized API client")
Expand All @@ -136,6 +138,13 @@ def name(self) -> str:
def filters(self) -> List[Type[BaseFilter]]:
return []

@property
def DesiredConfigurationSetInCFS(self) -> DesiredConfigurationSetInCFS:
"""
Shortcut to get a DesiredConfigurationSetInCFS filter with the cfs_client for this operator
"""
return DesiredConfigurationSetInCFS(self.client.cfs)

def run(self) -> NoReturn:
"""
The core method of the operator that periodically detects and acts on components.
Expand Down
7 changes: 3 additions & 4 deletions src/bos/operators/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,8 @@
import logging

from bos.common.values import Status
from bos.operators.utils.clients.cfs import set_cfs
from bos.operators.base import BaseOperator, main
from bos.operators.filters import BOSQuery, DesiredConfigurationSetInCFS, NOT
from bos.operators.filters import BOSQuery, NOT

LOGGER = logging.getLogger(__name__)

Expand All @@ -51,12 +50,12 @@ def name(self):
def filters(self):
return [
BOSQuery(enabled=True, status=Status.configuring),
NOT(DesiredConfigurationSetInCFS())
NOT(self.DesiredConfigurationSetInCFS)
]

def _act(self, components):
if components:
set_cfs(components, enabled=True)
self.client.cfs.components.set_cfs(components, enabled=True)
return components


Expand Down
11 changes: 6 additions & 5 deletions src/bos/operators/filters/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,10 @@
import re
from typing import List, Type

from bos.common.clients.cfs import CFSClient
from bos.common.utils import get_current_time, load_timestamp
from bos.operators.filters.base import BaseFilter, DetailsFilter, IDFilter, LocalFilter
from bos.operators.utils.clients.bos import BOSClient
from bos.operators.utils.clients.cfs import get_components_from_id_list as \
get_cfs_components_from_id_list
from bos.operators.utils.clients.hsm import get_components as get_hsm_components

LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -213,13 +212,15 @@ def _sanitize_kernel_parameters(self, parameter_string):
class DesiredConfigurationSetInCFS(LocalFilter):
""" Returns when desired configuration is set in CFS """

def __init__(self):
self.cfs_components_dict = {}
def __init__(self, cfs_client: CFSClient):
super().__init__()
self.cfs_components_dict = {}
self.cfs_client = cfs_client

def _filter(self, components: List[dict]) -> List[dict]:
component_ids = [component['id'] for component in components]
cfs_components = get_cfs_components_from_id_list(id_list=component_ids)
cfs_components = self.cfs_client.components.get_components_from_id_list(
id_list=component_ids)
self.cfs_components_dict = {
component['id']: component
for component in cfs_components
Expand Down
3 changes: 1 addition & 2 deletions src/bos/operators/power_on.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
using_sbps_check_kernel_parameters, components_by_id
from bos.common.values import Action, Status
from bos.operators.utils.clients.ims import tag_image
from bos.operators.utils.clients.cfs import set_cfs
from bos.operators.base import BaseOperator, main
from bos.operators.filters import BOSQuery, HSMState
from bos.server.dbs.boot_artifacts import record_boot_artifacts
Expand Down Expand Up @@ -83,7 +82,7 @@ def _act(self, components: Union[List[dict], None]):
raise Exception(
f"Error encountered setting BSS information: {e}") from e
try:
set_cfs(components, enabled=False, clear_state=True)
self.client.cfs.set_cfs(components, enabled=False, clear_state=True)
except Exception as e:
raise Exception(
f"Error encountered setting CFS information: {e}") from e
Expand Down
Loading

0 comments on commit e340171

Please sign in to comment.