From 04a4bc02082177df774500779a7aa93a26dd3db3 Mon Sep 17 00:00:00 2001 From: ananthu-kuttattu Date: Tue, 26 Mar 2024 12:01:54 +0530 Subject: [PATCH 1/2] Rv1.10.0 --- ChangeLog.md | 3 + PyPowerFlex/__init__.py | 4 +- PyPowerFlex/base_client.py | 58 +++++++------- PyPowerFlex/objects/__init__.py | 2 + PyPowerFlex/objects/deployment.py | 90 ++++++++++++++++++++++ PyPowerFlex/objects/firmware_repository.py | 51 ++++++++++++ PyPowerFlex/objects/service_template.py | 17 ++++ PyPowerFlex/utils.py | 3 + README.md | 1 + setup.py | 4 +- tests/__init__.py | 17 ++-- tests/test_deployment.py | 57 +++++++++++++- tests/test_firmware_repository.py | 41 ++++++++++ tests/test_service_template.py | 14 +++- tests/test_system.py | 4 +- 15 files changed, 325 insertions(+), 41 deletions(-) create mode 100644 PyPowerFlex/objects/firmware_repository.py create mode 100644 tests/test_firmware_repository.py diff --git a/ChangeLog.md b/ChangeLog.md index 69c469c..983155c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,8 @@ # PyPowerFlex Change Log +## Version 1.10.0 - released on 29/03/24 +- Added support for retrieving all the firmware repository, validating, deploying, editing, adding nodes and deleting a resource group from PowerFlex Manager. + ## Version 1.9.0 - released on 29/02/24 - Added support for retrieving managed devices, service templates and deployments from PowerFlex Manager. diff --git a/PyPowerFlex/__init__.py b/PyPowerFlex/__init__.py index 50e7e25..960a79e 100644 --- a/PyPowerFlex/__init__.py +++ b/PyPowerFlex/__init__.py @@ -47,7 +47,8 @@ class PowerFlexClient: 'replication_pair', 'service_template', 'managed_device', - 'deployment' + 'deployment', + 'firmware_repository' ) def __init__(self, @@ -98,6 +99,7 @@ def initialize(self): self.__add_storage_entity('service_template', objects.ServiceTemplate) self.__add_storage_entity('managed_device', objects.ManagedDevice) self.__add_storage_entity('deployment', objects.Deployment) + self.__add_storage_entity('firmware_repository', objects.FirmwareRepository) utils.init_logger(self.configuration.log_level) if version.parse(self.system.api_version()) < version.Version('3.0'): raise exceptions.PowerFlexClientException( diff --git a/PyPowerFlex/base_client.py b/PyPowerFlex/base_client.py index 6fa14b8..9366bc8 100644 --- a/PyPowerFlex/base_client.py +++ b/PyPowerFlex/base_client.py @@ -27,6 +27,9 @@ class Request: GET = "get" + POST = "post" + PUT = "put" + DELETE = "delete" def __init__(self, token, configuration): self.token = token @@ -67,39 +70,39 @@ def get_auth_headers(self, request_type=None): return {'Authorization': 'Bearer {0}'.format(self.token.get()), 'content-type': 'application/json'} - def send_get_request(self, url, **url_params): - request_url = self.base_url + url.format(**url_params) + def send_request(self, method, url, params=None, **url_params): + params = params or {} + request_url = f"{self.base_url}{url.format(**url_params)}" version = self.login() - request_params = {'url': request_url, - 'headers': self.get_auth_headers(request_type=self.GET), - 'verify': self.verify_certificate, - 'timeout': self.configuration.timeout} + request_params = { + 'headers': self.get_auth_headers(method), + 'verify': self.verify_certificate, + 'timeout': self.configuration.timeout + } if utils.is_version_3(version): - request_params['auth'] = (self.configuration.username, - self.token.get()) + request_params['auth'] = (self.configuration.username, self.token.get()) request_params['headers'] = None - r = requests.get(**request_params) + + if method in [self.PUT, self.POST]: + request_params['data'] = utils.prepare_params(params) + response = requests.request(method, request_url, **request_params) self.logout(version) - response = r.json() - return r, response + return response + + def send_get_request(self, url, params=None, **url_params): + response = self.send_request(self.GET, url, params, **url_params) + return response, response.json() def send_post_request(self, url, params=None, **url_params): - if params is None: - params = dict() - version = self.login() - request_url = self.base_url + url.format(**url_params) - r = requests.post(request_url, - auth=( - self.configuration.username, - self.token.get() - ), - headers=self.headers, - data=utils.prepare_params(params), - verify=self.verify_certificate, - timeout=self.configuration.timeout) - response = r.json() - self.logout(version) - return r, response + response = self.send_request(self.POST, url, params, **url_params) + return response, response.json() + + def send_put_request(self, url, params=None, **url_params): + response = self.send_request(self.PUT, url, params, **url_params) + return response, response.json() + + def send_delete_request(self, url, params=None, **url_params): + return self.send_request(self.DELETE, url, params, **url_params) def send_mdm_cluster_post_request(self, url, params=None, **url_params): if params is None: @@ -234,6 +237,7 @@ class EntityRequest(Request): service_template_url = '/V1/ServiceTemplate' managed_device_url = '/V1/ManagedDevice' deployment_url = '/V1/Deployment' + firmware_repository_url = '/V1/FirmwareRepository' entity_name = None @property diff --git a/PyPowerFlex/objects/__init__.py b/PyPowerFlex/objects/__init__.py index 0d5bcc6..57d5e46 100644 --- a/PyPowerFlex/objects/__init__.py +++ b/PyPowerFlex/objects/__init__.py @@ -29,6 +29,7 @@ from PyPowerFlex.objects.service_template import ServiceTemplate from PyPowerFlex.objects.managed_device import ManagedDevice from PyPowerFlex.objects.deployment import Deployment +from PyPowerFlex.objects.firmware_repository import FirmwareRepository __all__ = [ @@ -48,4 +49,5 @@ 'ServiceTemplate', 'ManagedDevice', 'Deployment', + 'FirmwareRepository', ] diff --git a/PyPowerFlex/objects/deployment.py b/PyPowerFlex/objects/deployment.py index a6bead0..a737f8c 100644 --- a/PyPowerFlex/objects/deployment.py +++ b/PyPowerFlex/objects/deployment.py @@ -50,3 +50,93 @@ def get(self, filters=None, full=None, include_devices=None, include_template=No LOG.error(msg) raise exceptions.PowerFlexClientException(msg) return response + + def get_by_id(self, deployment_id): + """ + Retrieve Deployment for specified ID. + :param deployment_id: Deployment ID. + :return: A dictionary containing the retrieved Deployment. + """ + r, response = self.send_get_request(f'{self.deployment_url}/{deployment_id}') + if r.status_code != requests.codes.ok: + msg = (f'Failed to retrieve deployment by id {deployment_id}. Error: {response}') + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + return response + + def validate(self, rg_data): + """ + Validates a new deployment. + Args: + rg_data (dict): The resource group data to be deployed. + Returns: + dict: The response from the deployment API. + Raises: + PowerFlexClientException: If the deployment fails. + """ + r, response = self.send_post_request(f'{self.deployment_url}/validate', rg_data) + if r.status_code != requests.codes.ok: + msg = (f'Failed to validate the deployment. Error: {response}') + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response + + def create(self, rg_data): + """ + Creates a new deployment. + Args: + rg_data (dict): The resource group data to be deployed. + Returns: + dict: The response from the deployment API. + Raises: + PowerFlexClientException: If the deployment fails. + """ + r, response = self.send_post_request(self.deployment_url, rg_data) + if r.status_code != requests.codes.ok: + msg = (f'Failed to create a new deployment. Error: {response}') + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response + + def edit(self, deployment_id, rg_data): + """ + Edit a deployment with the given ID using the provided data. + Args: + deployment_id (str): The ID of the deployment to edit. + rg_data (dict): The data to use for editing the deployment. + Returns: + dict: The response from the API. + Raises: + PowerFlexClientException: If the request fails. + """ + request_url = f'{self.deployment_url}/{deployment_id}' + r, response = self.send_put_request(request_url, rg_data) + + if r.status_code != requests.codes.ok: + msg = (f'Failed to edit the deployment. Error: {response}') + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response + + def delete(self, deployment_id): + """ + Deletes a deployment with the given ID. + Args: + deployment_id (str): The ID of the deployment to delete. + Returns: + str: The response from the delete request. + Raises: + exceptions.PowerFlexClientException: If the delete request fails. + """ + request_url = f'{self.deployment_url}/{deployment_id}' + response = self.send_delete_request(request_url) + + if response.status_code != requests.codes.no_content: + msg = (f'Failed to delete deployment. Error: {response}') + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response diff --git a/PyPowerFlex/objects/firmware_repository.py b/PyPowerFlex/objects/firmware_repository.py new file mode 100644 index 0000000..d8428cc --- /dev/null +++ b/PyPowerFlex/objects/firmware_repository.py @@ -0,0 +1,51 @@ +# Copyright (c) 2024 Dell Inc. or its subsidiaries. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import requests +from PyPowerFlex import base_client +from PyPowerFlex import exceptions +from PyPowerFlex import utils +LOG = logging.getLogger(__name__) + + +class FirmwareRepository(base_client.EntityRequest): + def get(self, filters=None, limit=None, offset=None, sort=None, related=False, bundles=False, components=False): + """ + Retrieve all firmware repository with filter, sort, pagination + :param filters: (Optional) The filters to apply to the results. + :param limit: (Optional) Page limit. + :param offset: (Optional) Pagination offset. + :param sort: (Optional) The field to sort the results by. + :param related: Whether to include related entities in the response. + :param bundles: Whether to include bundles in the response. + :param components: Whether to include components in the response. + :return: A list of dictionary containing the retrieved firmware repository. + """ + params = dict( + filter=filters, + sort=sort, + offset=offset, + limit=limit, + related=related, + bundles=bundles, + components=components + ) + r, response = self.send_get_request(utils.build_uri_with_params(self.firmware_repository_url, **params)) + if r.status_code != requests.codes.ok: + msg = (f'Failed to retrieve firmware repository. Error: {response}') + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + return response diff --git a/PyPowerFlex/objects/service_template.py b/PyPowerFlex/objects/service_template.py index 4025b4a..66d138c 100644 --- a/PyPowerFlex/objects/service_template.py +++ b/PyPowerFlex/objects/service_template.py @@ -46,3 +46,20 @@ def get(self, filters=None, full=None, limit=None, offset=None, sort=None, inclu LOG.error(msg) raise exceptions.PowerFlexClientException(msg) return response + + def get_by_id(self, service_template_id, for_deployment=False): + """ + Retrieve a Service Template by its ID. + :param service_template_id: The ID of the Service Template to retrieve. + :param for_deployment: (Optional) Whether to retrieve the Service Template for deployment. + :return: A dictionary containing the retrieved Service Template. + """ + url = f'{self.service_template_url}/{service_template_id}' + if for_deployment: + url += '?forDeployment=true' + r, response = self.send_get_request(url) + if r.status_code != requests.codes.ok: + msg = (f'Failed to retrieve service template by id {service_template_id}. Error: {response}') + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + return response diff --git a/PyPowerFlex/utils.py b/PyPowerFlex/utils.py index 9c2e425..e77ff1d 100644 --- a/PyPowerFlex/utils.py +++ b/PyPowerFlex/utils.py @@ -120,6 +120,9 @@ def prepare_params(params, dump=True): :return: prepared parameters """ + if not isinstance(params, dict): + return params + prepared = dict() for name, value in params.items(): if value is not None: diff --git a/README.md b/README.md index b43c8a9..e7ea5ab 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ python setup.py install * ManagedDevice * Deployment * ServiceTemplate +* FirmwareRepository #### Initialize PowerFlex client diff --git a/setup.py b/setup.py index f6e05d5..13d222b 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 Dell Inc. or its subsidiaries. +# Copyright (c) 2020 Dell Inc. or its subsidiaries. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -17,7 +17,7 @@ setup( name='PyPowerFlex', - version='1.9.0', + version='1.10.0', description='Python library for Dell PowerFlex', author='Ansible Team at Dell', author_email='ansible.team@dell.com', diff --git a/tests/__init__.py b/tests/__init__.py index 2ec9601..c6e1df7 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -34,7 +34,6 @@ class MockResponse(requests.Response): def __init__(self, content, status_code=200): super(MockResponse, self).__init__() - self._content = content self.request = mock.MagicMock() self.status_code = status_code @@ -103,6 +102,7 @@ def setUp(self): self.username, self.password, log_level=logging.DEBUG) + requests.request = self.get_mock_response self.get_mock = self.mock_object(requests, 'get', side_effect=self.get_mock_response) @@ -117,7 +117,6 @@ def mock_object(self, obj, attr_name, *args, **kwargs): Mocks the specified objects attribute with the given value. Automatically performs 'addCleanup' for the mock. """ - patcher = mock.patch.object(obj, attr_name, *args, **kwargs) result = patcher.start() self.addCleanup(patcher.stop) @@ -131,10 +130,11 @@ def http_response_mode(self, mode): yield self.__http_response_mode = previous_response_mode - def get_mock_response(self, url, mode=None, *args, **kwargs): + def get_mock_response(self, url, request_url=None, mode=None, *args, **kwargs): if mode is None: mode = self.__http_response_mode - api_path = url.split('/api')[1] + + api_path = url.split('/api')[1] if ('/api' in url) else request_url.split('/api')[1] try: if api_path == "/login": response = self.RESPONSE_MODE.Valid[0] @@ -155,7 +155,14 @@ def get_mock_response(self, url, mode=None, *args, **kwargs): ) ) if not isinstance(response, MockResponse): - response = MockResponse(response, 200) + response = self._get_mock_response(response) + response.request.url = url response.request.body = kwargs.get('data') return response + + def _get_mock_response(self, response): + if "204" in str(response): + return MockResponse(response, 204) + else: + return MockResponse(response, 200) diff --git a/tests/test_deployment.py b/tests/test_deployment.py index 0397944..b6cb93a 100644 --- a/tests/test_deployment.py +++ b/tests/test_deployment.py @@ -18,14 +18,19 @@ class TestDeploymentClient(tests.PyPowerFlexTestCase): + def setUp(self): super(TestDeploymentClient, self).setUp() self.client.initialize() - + self.RESPONSE_204 = "" + self.deployment_id = '8aaa03a88de961fa018de9c882d20301' + self.rg_data = {} self.MOCK_RESPONSES = { self.RESPONSE_MODE.Valid: { '/V1/Deployment': {}, - '/V1/Deployment?filter=co,name,Partial&includeDevices=False': {} + '/V1/Deployment?filter=co,name,Partial&includeDevices=False': {}, + f'/V1/Deployment/{self.deployment_id}': {}, + '/V1/Deployment/validate': {} } } @@ -35,7 +40,55 @@ def test_deployment_get(self): def test_deployment_get_with_query_params(self): self.client.deployment.get(filters=['co,name,Partial'], include_devices=False) + def test_deployment_get_by_id(self): + self.client.deployment.get_by_id(self.deployment_id) + def test_deployment_get_bad_status(self): with self.http_response_mode(self.RESPONSE_MODE.BadStatus): self.assertRaises(exceptions.PowerFlexClientException, self.client.deployment.get) + + def test_deployment_get_by_id_bad_status(self): + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.deployment.get_by_id, + self.deployment_id) + + def test_deployment_create(self): + self.client.deployment.create(self.rg_data) + + def test_deployment_create_bad_status(self): + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.deployment.create, + self.rg_data) + + def test_deployment_edit(self): + self.client.deployment.edit(self.deployment_id, self.rg_data) + + def test_deployment_edit_bad_status(self): + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.deployment.edit, + self.deployment_id, + self.rg_data) + + def test_deployment_delete(self): + url = f'/V1/Deployment/{self.deployment_id}' + self.MOCK_RESPONSES[self.RESPONSE_MODE.Valid][url] = self.RESPONSE_204 + self.client.deployment.delete(self.deployment_id) + + def test_deployment_delete_bad_status(self): + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.deployment.delete, + self.deployment_id) + + def test_deployment_validate(self): + self.client.deployment.validate(self.rg_data) + + def test_deployment_validate_bad_status(self): + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.deployment.validate, + self.rg_data) diff --git a/tests/test_firmware_repository.py b/tests/test_firmware_repository.py new file mode 100644 index 0000000..83d36cf --- /dev/null +++ b/tests/test_firmware_repository.py @@ -0,0 +1,41 @@ +# Copyright (c) 2024 Dell Inc. or its subsidiaries. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from PyPowerFlex import exceptions +import tests + + +class TestFirmwareRepositoryClient(tests.PyPowerFlexTestCase): + def setUp(self): + super(TestFirmwareRepositoryClient, self).setUp() + self.client.initialize() + + self.MOCK_RESPONSES = { + self.RESPONSE_MODE.Valid: { + '/V1/FirmwareRepository': {}, + '/V1/FirmwareRepository?related=False&bundles=False&components=False': {} + } + } + + def test_firmware_repository_get(self): + self.client.firmware_repository.get() + + def test_firmware_repository_get_with_query_params(self): + self.client.firmware_repository.get(related=False, bundles=False, components=False) + + def test_firmware_repository_get_bad_status(self): + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.firmware_repository.get) diff --git a/tests/test_service_template.py b/tests/test_service_template.py index 7f383fa..ee1ccfe 100644 --- a/tests/test_service_template.py +++ b/tests/test_service_template.py @@ -21,11 +21,12 @@ class TestServiceTemplateClient(tests.PyPowerFlexTestCase): def setUp(self): super(TestServiceTemplateClient, self).setUp() self.client.initialize() - + self.template_id = 1234 self.MOCK_RESPONSES = { self.RESPONSE_MODE.Valid: { '/V1/ServiceTemplate': {}, - '/V1/ServiceTemplate?filter=eq,draft,False&limit=10&includeAttachments=False': {} + '/V1/ServiceTemplate?filter=eq,draft,False&limit=10&includeAttachments=False': {}, + f'/V1/ServiceTemplate/{self.template_id}?forDeployment=true': {} } } @@ -39,3 +40,12 @@ def test_service_template_get_bad_status(self): with self.http_response_mode(self.RESPONSE_MODE.BadStatus): self.assertRaises(exceptions.PowerFlexClientException, self.client.service_template.get) + + def test_service_template_get_by_id(self): + self.client.service_template.get_by_id(self.template_id, for_deployment=True) + + def test_service_template_get_by_id_bad_status(self): + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.service_template.get_by_id, + self.template_id, for_deployment=True) diff --git a/tests/test_system.py b/tests/test_system.py index d1d15b3..9c57af9 100644 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -77,7 +77,7 @@ def setUp(self): def test_system_api_version(self): self.client.system.api_version() - self.assertEqual(5, self.get_mock.call_count) + self.assertEqual(4, self.get_mock.call_count) def test_system_api_version_bad_status(self): with self.http_response_mode(self.RESPONSE_MODE.BadStatus): @@ -95,7 +95,7 @@ def test_system_api_version_cached(self): self.client.system.api_version() self.client.system.api_version() self.client.system.api_version() - self.assertEqual(5, self.get_mock.call_count) + self.assertEqual(4, self.get_mock.call_count) def test_system_remove_cg_snapshots(self): self.client.system.remove_cg_snapshots(self.fake_system_id, From 793e06c87b1bd137a27712d38f84e22260144de5 Mon Sep 17 00:00:00 2001 From: ananthu-kuttattu Date: Tue, 26 Mar 2024 14:39:29 +0530 Subject: [PATCH 2/2] Revert year --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 13d222b..f08d540 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Dell Inc. or its subsidiaries. +# Copyright (c) 2024 Dell Inc. or its subsidiaries. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may