From 3996ca0bcb62e786334d5b963b1945a589aec78e Mon Sep 17 00:00:00 2001 From: Ryan Haasken Date: Mon, 18 Nov 2024 16:32:30 -0600 Subject: [PATCH] CRAYSAT-1896: Add `CFSClient` methods to get configurations, sessions Add methods to the `CFSV2Client` and `CFSV3Client` classes to get a list of all the CFS configurations and sessions. The CFS V3 version handles paging in the same way paging is handled for the `components` resource. This change is needed for the `sat bootprep` command to be updated to support CFS v2 and v3 because it looks through all the existing CFS configurations to determine if there are any existing CFS configurations of the same name. Test Description: Unit tests only so far. This will be tested by pulling it into the corresponding branch in the sat repository that adds CFS v2/v3 support to `sat bootprep`. --- CHANGELOG.md | 10 +++- csm_api_client/service/cfs.py | 77 ++++++++++++++++++++---- tests/service/test_cfs.py | 110 ++++++++++++++++++++++++++++++++++ 3 files changed, 185 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42082dc..954845d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,11 +25,17 @@ All notable changes to this project will be documented in this file. 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). +## [2.3.0] - 2024-11-18 + +### Added +- Added methods to the `CFSV2Client` and `CFSV3Client` classes to get a list of + all the configurations and sessions, handling paging for the CFS v3 API. + ## [2.2.4] - 2024-10-23 ### Updated -- Updated `poetry.lock` file to fetch latest version of `cray product catalog` - to which hash value remains consistent. +- Updated `poetry.lock` file to fetch latest version of `cray product catalog` + to which hash value remains consistent. ## [2.2.3] - 2024-10-14 diff --git a/csm_api_client/service/cfs.py b/csm_api_client/service/cfs.py index b37f6b6..868312d 100644 --- a/csm_api_client/service/cfs.py +++ b/csm_api_client/service/cfs.py @@ -1474,6 +1474,34 @@ def get_components(self, params: Dict = None) -> Generator[Dict, None, None]: """ pass + @abstractmethod + def get_configurations(self, params: Dict = None) -> Generator[Dict, None, None]: + """Get all the CFS configurations. + + This method must handle paging if necessary. + + Args: + params: the parameters to pass to the GET on configurations + + Yields: + The CFS configurations. + """ + pass + + @abstractmethod + def get_sessions(self, params: Dict = None) -> Generator[Dict, None, None]: + """Get all the CFS sessions. + + This method must handle paging if necessary. + + Args: + params: the parameters to pass to the GET on sessions + + Yields: + The CFS sessions. + """ + pass + def get_component_ids_using_config(self, config_name: str) -> List[str]: """Get a list of CFS components using the given CFS configuration. @@ -1564,14 +1592,29 @@ class CFSV2Client(CFSClientBase): def join_words(*words: str) -> str: return ''.join([words[0].lower()] + [word.capitalize() for word in words[1:]]) - def get_components(self, params: Dict = None) -> Generator[Dict, None, None]: + def get_resource(self, resource: str, params: Dict = None) -> Generator[Dict, None, None]: + """Get a resource from the CFS API. + + Args: + resource: the name of the resource to get (e.g. 'components') + params: the parameters to pass to the GET on the resource + """ try: - yield from self.get('components', params=params).json() + yield from self.get(resource, params=params).json() except APIError as err: - raise APIError(f'Failed to get CFS components: {err}') + raise APIError(f'Failed to get CFS {resource}: {err}') except ValueError as err: raise APIError(f'Failed to parse JSON in response from CFS when getting ' - f'components: {err}') + f'{resource}: {err}') + + def get_components(self, params: Dict = None) -> Generator[Dict, None, None]: + yield from self.get_resource('components', params=params) + + def get_configurations(self, params: Dict = None) -> Generator[Dict, None, None]: + yield from self.get_resource('configurations', params=params) + + def get_sessions(self, params: Dict = None) -> Generator[Dict, None, None]: + yield from self.get_resource('sessions', params=params) # Create an alias for CFSClient that points at CFSV2Client to preserve backwards compatibility @@ -1586,19 +1629,33 @@ class CFSV3Client(CFSClientBase): def join_words(*words: str) -> str: return '_'.join([word.lower() for word in words]) - def get_components(self, params: Dict = None) -> Generator[Dict, None, None]: + def get_paged_resource(self, resource: str, params: Dict = None) -> Generator[Dict, None, None]: + """Get a paged resource from the CFS API. + + Args: + resource: the name of the resource to get (e.g. 'components') + params: the parameters to pass to the GET on the resource + """ # On the first request, pass in user-specified parameters next_params = params try: while True: - response = self.get('components', params=next_params).json() - yield from response['components'] - # The CFS API preserves user-specified parameters and adds pagination parameters + response = self.get(resource, params=next_params).json() + yield from response[resource] next_params = response.get('next') if not next_params: break except APIError as err: - raise APIError(f'Failed to get CFS components: {err}') + raise APIError(f'Failed to get CFS {resource}: {err}') except ValueError as err: raise APIError(f'Failed to parse JSON in response from CFS when getting ' - f'components: {err}') + f'{resource}: {err}') + + def get_components(self, params: Dict = None) -> Generator[Dict, None, None]: + yield from self.get_paged_resource('components', params=params) + + def get_configurations(self, params: Dict = None) -> Generator[Dict, None, None]: + yield from self.get_paged_resource('configurations', params=params) + + def get_sessions(self, params: Dict = None) -> Generator[Dict, None, None]: + yield from self.get_paged_resource('sessions', params=params) diff --git a/tests/service/test_cfs.py b/tests/service/test_cfs.py index 0b9b77f..1aac40c 100644 --- a/tests/service/test_cfs.py +++ b/tests/service/test_cfs.py @@ -1885,4 +1885,114 @@ def test_get_components_unpaged(self): mock_get.assert_has_calls([ call('components', params=None), call().json() + ]) + + def test_get_configurations_paged(self): + """Test get_configurations method of CFSV3Client with paged results""" + cfs_client = CFSV3Client(Mock()) + configurations = [ + {'name': 'config-1'}, + {'name': 'config-2'}, + {'name': 'config-3'}, + {'name': 'config-4'}, + {'name': 'config-5'}, + ] + + base_params = {'limit': 2} + with patch.object(cfs_client, 'get') as mock_get: + mock_get.return_value.json.side_effect = [ + {'configurations': configurations[:2], 'next': {'limit': 2, 'after': 'config-2'}}, + {'configurations': configurations[2:4], 'next': {'limit': 2, 'after': 'config-4'}}, + {'configurations': [configurations[4]], 'next': None} + ] + + result = list(cfs_client.get_configurations(params=base_params)) + + self.assertEqual(configurations, result) + mock_get.assert_has_calls([ + call('configurations', params=base_params), + call().json(), + call('configurations', params={'limit': 2, 'after': 'config-2'}), + call().json(), + call('configurations', params={'limit': 2, 'after': 'config-4'}), + call().json() + ]) + + def test_get_configurations_unpaged(self): + """Test get_configurations method of CFSV3Client when results are not paged""" + cfs_client = CFSV3Client(Mock()) + configurations = [ + {'name': 'config-1'}, + {'name': 'config-2'}, + {'name': 'config-3'}, + {'name': 'config-4'}, + {'name': 'config-5'}, + ] + + with patch.object(cfs_client, 'get') as mock_get: + mock_get.return_value.json.side_effect = [ + {'configurations': configurations, 'next': None} + ] + + result = list(cfs_client.get_configurations()) + + self.assertEqual(configurations, result) + mock_get.assert_has_calls([ + call('configurations', params=None), + call().json() + ]) + + def test_get_sessions_paged(self): + """Test get_sessions method of CFSV3Client with paged results""" + cfs_client = CFSV3Client(Mock()) + sessions = [ + {'name': 'session-1'}, + {'name': 'session-2'}, + {'name': 'session-3'}, + {'name': 'session-4'}, + {'name': 'session-5'}, + ] + + base_params = {'limit': 2} + with patch.object(cfs_client, 'get') as mock_get: + mock_get.return_value.json.side_effect = [ + {'sessions': sessions[:2], 'next': {'limit': 2, 'after': 'session-2'}}, + {'sessions': sessions[2:4], 'next': {'limit': 2, 'after': 'session-4'}}, + {'sessions': [sessions[4]], 'next': None} + ] + + result = list(cfs_client.get_sessions(params=base_params)) + + self.assertEqual(sessions, result) + mock_get.assert_has_calls([ + call('sessions', params=base_params), + call().json(), + call('sessions', params={'limit': 2, 'after': 'session-2'}), + call().json(), + call('sessions', params={'limit': 2, 'after': 'session-4'}), + call().json() + ]) + + def test_get_sessions_unpaged(self): + """Test get_sessions method of CFSV3Client when results are not paged""" + cfs_client = CFSV3Client(Mock()) + sessions = [ + {'name': 'session-1'}, + {'name': 'session-2'}, + {'name': 'session-3'}, + {'name': 'session-4'}, + {'name': 'session-5'}, + ] + + with patch.object(cfs_client, 'get') as mock_get: + mock_get.return_value.json.side_effect = [ + {'sessions': sessions, 'next': None} + ] + + result = list(cfs_client.get_sessions()) + + self.assertEqual(sessions, result) + mock_get.assert_has_calls([ + call('sessions', params=None), + call().json() ]) \ No newline at end of file