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-9225: Move CFS client to new paradigm #402

Open
wants to merge 3 commits into
base: casmcms-9225-06-bss-client
Choose a base branch
from
Open
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
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."
mharding-hpe marked this conversation as resolved.
Show resolved Hide resolved
)
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.components.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
Loading