Skip to content

Commit

Permalink
CASMCMS-9225: Move BOS 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 18c373b commit bccce47
Show file tree
Hide file tree
Showing 23 changed files with 285 additions and 342 deletions.
25 changes: 25 additions & 0 deletions src/bos/common/clients/bos/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#
# 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 .client import BOSClient
121 changes: 121 additions & 0 deletions src/bos/common/clients/bos/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#
# 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.tenant_utils import get_new_tenant_header
from bos.common.utils import PROTOCOL

LOGGER = logging.getLogger(__name__)

API_VERSION = 'v2'
SERVICE_NAME = 'cray-bos'
BASE_BOS_ENDPOINT = f"{PROTOCOL}://{SERVICE_NAME}/{API_VERSION}"


class BaseBosEndpoint(BaseEndpoint, ABC):
"""
This base class provides generic access to the BOS API.
The individual endpoint needs to be overridden for a specific endpoint.
"""
BASE_ENDPOINT = BASE_BOS_ENDPOINT


class BaseBosNonTenantAwareEndpoint(BaseBosEndpoint, ABC):
"""
This base class provides generic access to the BOS API for non-tenant-aware endpoints
The individual endpoint needs to be overridden for a specific endpoint.
"""

def get_item(self, item_id):
"""Get information for a single BOS item"""
return self.get(uri=item_id)

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

def update_item(self, item_id, data):
"""Update information for a single BOS item"""
return self.patch(uri=item_id, json=data)

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

def put_items(self, data):
"""Put information for multiple BOS Items"""
return self.put(json=data)

def delete_items(self, **kwargs):
"""Delete information for multiple BOS items"""
return self.delete(params=kwargs)


class BaseBosTenantAwareEndpoint(BaseBosEndpoint, ABC):
"""
This base class provides generic access to the BOS API for tenant aware endpoints.
The individual endpoint needs to be overridden for a specific endpoint.
"""

def get_item(self, item_id, tenant):
"""Get information for a single BOS item"""
return self.get(uri=item_id, headers=get_new_tenant_header(tenant))

def get_items(self, **kwargs):
"""Get information for all BOS items"""
headers = None
if "tenant" in kwargs:
tenant = kwargs.pop("tenant")
headers = get_new_tenant_header(tenant)
return self.get(params=kwargs, headers=headers)

def update_item(self, item_id, tenant, data):
"""Update information for a single BOS item"""
return self.patch(uri=item_id,
json=data,
headers=get_new_tenant_header(tenant))

def update_items(self, tenant, data):
"""Update information for multiple BOS items"""
return self.patch(json=data, headers=get_new_tenant_header(tenant))

def post_item(self, item_id, tenant, data=None):
"""Post information for a single BOS item"""
return self.post(uri=item_id,
json=data,
headers=get_new_tenant_header(tenant))

def put_items(self, tenant, data):
"""Put information for multiple BOS items"""
return self.put(json=data, headers=get_new_tenant_header(tenant))

def delete_items(self, **kwargs):
"""Delete information for multiple BOS items"""
headers = None
if "tenant" in kwargs:
tenant = kwargs.pop("tenant")
headers = get_new_tenant_header(tenant)
return self.delete(params=kwargs, headers=headers)
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# MIT License
#
# (C) Copyright 2021-2022 Hewlett Packard Enterprise Development LP
# (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"),
Expand All @@ -21,16 +21,23 @@
# 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 .components import ComponentEndpoint
from .sessions import SessionEndpoint
from .session_templates import SessionTemplateEndpoint
from .sessions_status import SessionStatusEndpoint


class BOSClient:
class BOSClient(APIClient):

@property
def components(self) -> ComponentEndpoint:
return self.get_endpoint(ComponentEndpoint)

@property
def sessions(self) -> SessionEndpoint:
return self.get_endpoint(SessionEndpoint)

def __init__(self):
self.components = ComponentEndpoint()
self.sessions = SessionEndpoint()
self.session_status = SessionStatusEndpoint()
self.session_templates = SessionTemplateEndpoint()
@property
def session_templates(self) -> SessionTemplateEndpoint:
return self.get_endpoint(SessionTemplateEndpoint)
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,31 @@
#
import logging

from .base import BaseBosEndpoint
from .base import BaseBosNonTenantAwareEndpoint
from .options import options

LOGGER = logging.getLogger(__name__)


class ComponentEndpoint(BaseBosEndpoint):
class ComponentEndpoint(BaseBosNonTenantAwareEndpoint):
ENDPOINT = __name__.lower().rsplit('.', maxsplit=1)[-1]

def get_component(self, component_id):
return self.get_item(component_id)

def get_components(self, **kwargs):
return self.get_items(**kwargs)
page_size = options.max_component_batch_size
if page_size == 0:
return self.get_items(**kwargs)
next_page = self.get_items(page_size=page_size, **kwargs)
results = next_page
while len(next_page) == page_size:
last_id = next_page[-1]["id"]
next_page = self.get_items(page_size=page_size,
start_after_id=last_id,
**kwargs)
results.extend(next_page)
return results

def update_component(self, component_id, data):
return self.update_item(component_id, data)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,20 @@
#
import logging
import json
from typing import Optional

from requests.exceptions import HTTPError, ConnectionError
import requests
from requests.exceptions import HTTPError
from requests.exceptions import ConnectionError as RequestsConnectionError
from urllib3.exceptions import MaxRetryError

from bos.common.options import OptionsCache
from bos.common.utils import exc_type_msg, requests_retry_session
from bos.operators.utils.clients.bos.base import BASE_ENDPOINT
from bos.common.utils import exc_type_msg, retry_session_get
from bos.common.clients.bos.base import BASE_BOS_ENDPOINT

LOGGER = logging.getLogger(__name__)
__name = __name__.lower().rsplit('.', maxsplit=1)[-1]
ENDPOINT = f"{BASE_ENDPOINT}/{__name}"
ENDPOINT = f"{BASE_BOS_ENDPOINT}/{__name}"


class Options(OptionsCache):
Expand All @@ -44,15 +47,14 @@ class Options(OptionsCache):
result in network calls.
"""

def _get_options(self) -> dict:
def _get_options(self, session: Optional[requests.Session] = None) -> dict:
"""Retrieves the current options from the BOS api"""
session = requests_retry_session()
LOGGER.debug("GET %s", ENDPOINT)
try:
response = session.get(ENDPOINT)
response.raise_for_status()
return json.loads(response.text)
except (ConnectionError, MaxRetryError) as e:
with retry_session_get(ENDPOINT, session=session) as response:
response.raise_for_status()
return json.loads(response.text)
except (RequestsConnectionError, MaxRetryError) as e:
LOGGER.error("Unable to connect to BOS: %s", exc_type_msg(e))
except HTTPError as e:
LOGGER.error("Unexpected response from BOS: %s", exc_type_msg(e))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# MIT License
#
# (C) Copyright 2021-2023 Hewlett Packard Enterprise Development LP
# (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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,10 @@ def update_session(self, session_id, tenant, data):

def delete_sessions(self, **kwargs):
return self.delete_items(**kwargs)

def post_session_status(self, session_id, tenant):
"""
Post information for a single BOS Session status.
This basically saves the BOS Session status to the database.
"""
return self.post_item(f'{session_id}/status', tenant)
2 changes: 1 addition & 1 deletion src/bos/common/clients/cfs/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
# OTHER DEALINGS IN THE SOFTWARE.
#
from bos.common.clients.api_client import APIClient
from bos.operators.utils.clients.bos.options import options
from bos.common.clients.bos.options import options

from .components import ComponentEndpoint

Expand Down
36 changes: 34 additions & 2 deletions src/bos/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@
#

# Standard imports
from contextlib import nullcontext
import datetime
from functools import partial
import re
import traceback
from typing import List, Unpack
from typing import Iterator, List, Optional, Unpack

# Third party imports
from dateutil.parser import parse
import requests
import requests_retry_session as rrs

PROTOCOL = 'http'
Expand Down Expand Up @@ -76,6 +78,11 @@ def duration_to_timedelta(timestamp: str):
read_timeout=10)


retry_session_manager = partial(rrs.retry_session_manager,
protocol=PROTOCOL,
**DEFAULT_RETRY_ADAPTER_ARGS)


class RetrySessionManager(rrs.RetrySessionManager):
"""
Just sets the default values we use for our requests sessions
Expand All @@ -90,7 +97,32 @@ def __init__(self,
super().__init__(protocol=protocol, **adapter_kwargs)


requests_retry_session = partial(rrs.requests_retry_session,
def retry_session(
session: Optional[requests.Session] = None,
protocol: Optional[str] = None,
adapter_kwargs: Optional[rrs.RequestsRetryAdapterArgs] = None
) -> Iterator[requests.Session]:
if session is not None:
return nullcontext(session)
kwargs = adapter_kwargs or {}
if protocol is not None:
return retry_session_manager(protocol=protocol, **kwargs) # pylint: disable=redundant-keyword-arg
return retry_session_manager(**kwargs)


def retry_session_get(*get_args,
session: Optional[requests.Session] = None,
protocol: Optional[str] = None,
adapter_kwargs: Optional[
rrs.RequestsRetryAdapterArgs] = None,
**get_kwargs) -> Iterator[requests.Response]:
with retry_session(session=session,
protocol=protocol,
adapter_kwargs=adapter_kwargs) as _session:
return _session.get(*get_args, **get_kwargs)


requests_retry_session = partial(rrs.requests_retry_session,
session=None,
protocol=PROTOCOL,
**DEFAULT_RETRY_ADAPTER_ARGS)
Expand Down
6 changes: 3 additions & 3 deletions src/bos/operators/actual_state_cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
#
import logging

from bos.common.clients.bos.options import options
from bos.common.utils import duration_to_timedelta
from bos.common.values import EMPTY_ACTUAL_STATE
from bos.operators.utils.clients.bos.options import options
from bos.operators.base import BaseOperator, main
from bos.operators.filters import BOSQuery, ActualStateAge, ActualBootStateIsSet

Expand All @@ -53,7 +53,7 @@ def name(self):
@property
def filters(self):
return [
BOSQuery(),
self.BOSQuery(),
ActualBootStateIsSet(),
ActualStateAge(seconds=duration_to_timedelta(
options.component_actual_state_ttl).total_seconds())
Expand All @@ -69,7 +69,7 @@ def _act(self, components):
if data:
LOGGER.info('Found %d components that require updates', len(data))
LOGGER.debug('Calling to update with payload: %s', data)
self.bos_client.components.update_components(data)
self.client.bos.components.update_components(data)
return components


Expand Down
Loading

0 comments on commit bccce47

Please sign in to comment.