├── .github └── workflows │ ├── pylint.yml │ └── pytest.yml ├── .gitignore ├── .travis.yml ├── ChangeLog.md ├── LICENSE ├── PyPowerFlex ├── __init__.py ├── base_client.py ├── configuration.py ├── constants.py ├── exceptions.py ├── objects │ ├── __init__.py │ ├── acceleration_pool.py │ ├── deployment.py │ ├── device.py │ ├── fault_set.py │ ├── firmware_repository.py │ ├── host.py │ ├── managed_device.py │ ├── protection_domain.py │ ├── replication_consistency_group.py │ ├── replication_pair.py │ ├── sdc.py │ ├── sds.py │ ├── sdt.py │ ├── service_template.py │ ├── snapshot_policy.py │ ├── storage_pool.py │ ├── system.py │ ├── utility.py │ └── volume.py ├── token.py └── utils.py ├── README.md ├── catalog-info.yaml ├── requirements.txt ├── setup.py ├── test-requirements.txt ├── tests ├── __init__.py ├── requirements.txt ├── test_acceleration_pool.py ├── test_base.py ├── test_deployment.py ├── test_device.py ├── test_fault_set.py ├── test_firmware_repository.py ├── test_host.py ├── test_managed_device.py ├── test_protection_domain.py ├── test_replication_consistency_group.py ├── test_replication_pair.py ├── test_sdc.py ├── test_sds.py ├── test_sdt.py ├── test_service_template.py ├── test_snapshot_policy.py ├── test_storage_pool.py ├── test_system.py ├── test_utility.py └── test_volume.py └── tox.ini /.github/workflows/pylint.yml: -------------------------------------------------------------------------------- 1 | name: Run Lint Check via Pylint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | name: lint 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | python-version: ['3.9', '3.10', '3.11', '3.12'] 13 | 14 | steps: 15 | - name: Checkout the source code 16 | uses: actions/checkout@v3 17 | 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | 23 | - name: Install dependencies 24 | run: python -m pip install --upgrade pip && pip install pylint 25 | 26 | - name: Install requirements 27 | run: | 28 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 29 | 30 | - name: Analyzing the code with pylint 31 | run: pylint $(git ls-files '*.py') 32 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: Run Unit Test via Pytest 2 | 3 | concurrency: 4 | group: ${{ github.head_ref || github.run_id }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | # Run CI against all pushes (direct commits, also merged PRs), Pull Requests 9 | workflow_dispatch: 10 | push: 11 | branches: 12 | - main 13 | pull_request: 14 | # Runs CI on every day (at 06:00 UTC) 15 | schedule: 16 | - cron: '0 6 * * *' 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | python-version: ['3.9', '3.10', '3.11', '3.12'] 24 | 25 | steps: 26 | - name: Check out the code 27 | uses: actions/checkout@v3 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v4 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - name: Install dependencies 33 | run: | 34 | python -m pip install --upgrade pip 35 | pip install testtools requests pytest pytest-coverage coverage 36 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 37 | - name: Run Unit Test and Generate report 38 | run: | 39 | coverage run -m pytest -v tests/test_*.py 40 | - name: Upload Coverage to Codecov 41 | uses: codecov/codecov-action@v5 42 | if: ${{ matrix.python-version == '3.12' }} 43 | env: 44 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | __pycache__ 3 | 4 | # pytest coverage 5 | .coverage 6 | htmlcov 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | language: python 3 | branches: 4 | only: 5 | - master 6 | - develop 7 | - "/^feature.*$/" 8 | matrix: 9 | include: 10 | - python: 3.8 11 | - python: 3.7 12 | - python: 3.6 13 | - python: 3.5 14 | install: 15 | - pip install tox 16 | script: 17 | - tox 18 | notifications: 19 | slack: 20 | secure: UngZexgY65GWMHKnJ23Y5zzPilYNyURAQJVkimcd811KmeAcKmmn3jmYY71NSZ5h70zNlEcuxPe8lAMaA04w6rJKZkeTUX/cIToq7qW2cAipGh3zUQC+F3UDQpsH0bJPMnPyGq+rgjjpztmcT2LVFrUGTAt+dDThdnLlIMFrjzqkM+/dVXCJ6R9TJeivRJGJmAGWKhe0VJOopONMsUjqwYlg+SGSSpW/MSqC2q5sD6rPtb+EA+lwPvMj0B0jiP3EDFhmVNcvWWcD5iAl/YFv2vtprX+SSUMCSdv4oMf71ohb1DaFlYBtRXSIu7xygrjATIxrAe/WH9QkptTllwYqPOf4Lk2oUIW+/apAW706XnrKmfSwFVHZ5mZ/6KLVfKZTsd3B1uMH/Wza576shsJ8gTbXY8pfpPeoxWZGTqcH8YwvT0KRbV/e8s/rvHqllbZGZgmQGORdRvvwFIBjF0lLet1TFeVKZgExOab47OFb0VuMV64gIrDnNb0i9+Jna08ddKEybry6jr92o8xQeglzsD3CCfkkCUX2ejtz0VIX/RUxLUuN+HO/c9pJjbXID/A+VYDSxzT9BgTl7slVVm2Vjxqv/vgg9tCb7inzjnlA6jQOCEXooYx9jX5Azi7uHGDWoun5qSverma3jeia2Cdh/7VWc1EmrnFmYv5oUjI9LHQ= 21 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # PyPowerFlex Change Log 2 | 3 | ## Version 1.14.0 - released on 05/12/24 4 | - Added support for managing NVME over TCP entities including SDT and NVMe Host. 5 | 6 | ## Version 1.13.0 - released on 28/10/24 7 | - Fixed storage pool get_sdss function to return the correct data. 8 | 9 | ## Version 1.12.0 - released on 31/05/24 10 | - Enhanced the storage pool module by adding support for more functionalities. 11 | - Added support for PowerFlex Onyx(4.6.x). 12 | 13 | ## Version 1.11.0 - released on 30/04/24 14 | - Added support to query selected statistics to all relevant objects/entities from PowerFlex Manager. 15 | 16 | ## Version 1.10.0 - released on 29/03/24 17 | - Added support for retrieving all the firmware repository, validating, deploying, editing, adding nodes and deleting a resource group from PowerFlex Manager. 18 | 19 | ## Version 1.9.0 - released on 29/02/24 20 | - Added support for retrieving managed devices, service templates and deployments from PowerFlex Manager. 21 | 22 | ## Version 1.8.0 - released on 30/06/23 23 | - Added block provisioning operations includes modifying performance profile in SDC, adding statistics data for snapshot policy, adding gateway configuration details for system, failover, restore, reverse, switchover, and sync operations in replication consistency group. 24 | 25 | ## Version 1.7.0 - released on 31/03/23 26 | - Added block provisioning operations includes getting details, adding, pause, resume and removing a replication pair. 27 | 28 | ## Version 1.6.0 - released on 28/12/22 29 | - Added block provisioning operations includes getting details, creating, modifying, creating snapshots, pause, resume, freeze, unfreeze, 30 | activate, inactivate and deleting a replication consistency group. 31 | 32 | ## Version 1.5.0 - released on 27/09/22 33 | - Added support for 4.0.x release of PowerFlex OS, adding statistics data for storage pool and volume objects. 34 | 35 | ## Version 1.4.0 - released on 28/06/22 36 | - Added configuration operations includes adding/removing of standby MDM, change cluster ownership, change cluster mode, modify performance profile, rename MDM, modify MDM's virtual interface and getting details of MDM cluster entities. 37 | 38 | ## Version 1.3.0 - released on 25/03/22 39 | - Added block provisioning operations includes managing protection domain and getting high level facts about this entity. 40 | 41 | ## Version 1.2.0 - released on 24/09/21 42 | - Added block provisioning operations includes managing SDS, device, acceleration pool and getting high level facts about all these entities. 43 | 44 | ## Version 1.1.0 - released on 24/03/21 45 | - Added block provisioning operations includes managing volume, snapshot, snapshot policy, storage pool, SDC and getting high level facts about all these entities. 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Dell Technologies 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /PyPowerFlex/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """This module is used for the initialization of PowerFlex Client.""" 17 | 18 | # pylint: disable=invalid-name,too-many-arguments,too-many-positional-arguments 19 | 20 | from packaging import version 21 | 22 | from PyPowerFlex import configuration 23 | from PyPowerFlex import exceptions 24 | from PyPowerFlex import objects 25 | from PyPowerFlex import token 26 | from PyPowerFlex import utils 27 | 28 | 29 | __all__ = [ 30 | 'PowerFlexClient' 31 | ] 32 | 33 | 34 | class PowerFlexClient: 35 | """ 36 | Client class for interacting with PowerFlex API. 37 | 38 | This class initializes the client with the provided configuration and provides 39 | access to the various storage entities available in the PowerFlex system. 40 | """ 41 | __slots__ = ( 42 | '__is_initialized', 43 | 'configuration', 44 | 'token', 45 | 'device', 46 | 'fault_set', 47 | 'protection_domain', 48 | 'sdc', 49 | 'sds', 50 | 'sdt', 51 | 'snapshot_policy', 52 | 'storage_pool', 53 | 'acceleration_pool', 54 | 'system', 55 | 'volume', 56 | 'utility', 57 | 'replication_consistency_group', 58 | 'replication_pair', 59 | 'service_template', 60 | 'managed_device', 61 | 'deployment', 62 | 'firmware_repository', 63 | 'host' 64 | ) 65 | 66 | def __init__(self, 67 | gateway_address=None, 68 | gateway_port=443, 69 | username=None, 70 | password=None, 71 | verify_certificate=False, 72 | certificate_path=None, 73 | timeout=120, 74 | log_level=None): 75 | self.configuration = configuration.Configuration(gateway_address, 76 | gateway_port, 77 | username, 78 | password, 79 | verify_certificate, 80 | certificate_path, 81 | timeout, 82 | log_level) 83 | self.token = token.Token() 84 | self.__is_initialized = False 85 | 86 | def __getattr__(self, item): 87 | if not self.__is_initialized and item in self.__slots__: 88 | raise exceptions.ClientNotInitialized 89 | return super().__getattribute__(item) 90 | 91 | def __add_storage_entity(self, attr_name, entity_class): 92 | setattr(self, attr_name, entity_class(self.token, self.configuration)) 93 | 94 | def initialize(self): 95 | """ 96 | Initializes the client. 97 | 98 | Raises: 99 | PowerFlexClientException: If the PowerFlex API version is lower than 3.0. 100 | """ 101 | self.configuration.validate() 102 | self.__add_storage_entity('device', objects.Device) 103 | self.__add_storage_entity('fault_set', objects.FaultSet) 104 | self.__add_storage_entity('protection_domain', 105 | objects.ProtectionDomain) 106 | self.__add_storage_entity('sdc', objects.Sdc) 107 | self.__add_storage_entity('sds', objects.Sds) 108 | self.__add_storage_entity('sdt', objects.Sdt) 109 | self.__add_storage_entity('snapshot_policy', objects.SnapshotPolicy) 110 | self.__add_storage_entity('storage_pool', objects.StoragePool) 111 | self.__add_storage_entity('acceleration_pool', 112 | objects.AccelerationPool) 113 | self.__add_storage_entity('system', objects.System) 114 | self.__add_storage_entity('volume', objects.Volume) 115 | self.__add_storage_entity('utility', objects.PowerFlexUtility) 116 | self.__add_storage_entity( 117 | 'replication_consistency_group', 118 | objects.ReplicationConsistencyGroup) 119 | self.__add_storage_entity('replication_pair', objects.ReplicationPair) 120 | self.__add_storage_entity('service_template', objects.ServiceTemplate) 121 | self.__add_storage_entity('managed_device', objects.ManagedDevice) 122 | self.__add_storage_entity('deployment', objects.Deployment) 123 | self.__add_storage_entity( 124 | 'firmware_repository', 125 | objects.FirmwareRepository) 126 | self.__add_storage_entity('host', objects.Host) 127 | utils.init_logger(self.configuration.log_level) 128 | if version.parse(self.system.api_version()) < version.Version('3.0'): 129 | raise exceptions.PowerFlexClientException( 130 | 'PowerFlex (VxFlex OS) versions lower than ' 131 | '3.0 are not supported.' 132 | ) 133 | self.__is_initialized = True 134 | -------------------------------------------------------------------------------- /PyPowerFlex/configuration.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """This module is used for the configuration of the client.""" 17 | 18 | # pylint: disable=too-many-instance-attributes,too-many-arguments,too-many-positional-arguments,too-few-public-methods 19 | 20 | from PyPowerFlex import exceptions 21 | 22 | class Configuration: 23 | """ 24 | Configuration class for the PyPowerFlex library. 25 | """ 26 | def __init__(self, 27 | gateway_address=None, 28 | gateway_port=443, 29 | username=None, 30 | password=None, 31 | verify_certificate=False, 32 | certificate_path=None, 33 | timeout=120, 34 | log_level=None): 35 | """ 36 | Initializes the Configuration class. 37 | """ 38 | self.gateway_address = gateway_address 39 | self.gateway_port = gateway_port 40 | self.username = username 41 | self.password = password 42 | self.verify_certificate = verify_certificate 43 | self.certificate_path = certificate_path 44 | self.timeout = timeout 45 | self.log_level = log_level 46 | 47 | def validate(self): 48 | """ 49 | Validates the configuration. 50 | 51 | :raises exceptions.InvalidConfiguration: If any of the required parameters are not set. 52 | """ 53 | if not all( 54 | [ 55 | self.gateway_address, 56 | self.gateway_port, 57 | self.username, 58 | self.password 59 | ] 60 | ): 61 | raise exceptions.InvalidConfiguration( 62 | 'The following parameters must be set: ' 63 | 'gateway_address, gateway_port, username, password.' 64 | ) 65 | -------------------------------------------------------------------------------- /PyPowerFlex/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """This module contains the definitions of the exceptions used in the code.""" 17 | 18 | # pylint: disable=super-init-not-called 19 | 20 | class PowerFlexClientException(Exception): 21 | """ 22 | Base class for all exceptions raised by the PowerFlexClient. 23 | """ 24 | def __init__(self, message, response=None): 25 | self.message = message 26 | self.response = response 27 | 28 | def __str__(self): 29 | return self.message 30 | 31 | 32 | class ClientNotInitialized(PowerFlexClientException): 33 | """ 34 | Exception raised when the PowerFlexClient is not initialized. 35 | """ 36 | def __init__(self): 37 | self.message = ( 38 | 'PowerFlex Client is not initialized. ' 39 | 'Call `.initialize()` to proceed.' 40 | ) 41 | 42 | 43 | class InvalidConfiguration(PowerFlexClientException): 44 | """ 45 | Exception raised when the configuration is invalid. 46 | """ 47 | 48 | 49 | class FieldsNotFound(PowerFlexClientException): 50 | """ 51 | Exception raised when the required fields are not found. 52 | """ 53 | 54 | 55 | class InvalidInput(PowerFlexClientException): 56 | """ 57 | Exception raised when the input is invalid. 58 | """ 59 | 60 | 61 | class PowerFlexFailCreating(PowerFlexClientException): 62 | """ 63 | Exception raised when creating a PowerFlex entity fails. 64 | """ 65 | base = 'Failed to create PowerFlex {entity}.' 66 | 67 | def __init__(self, entity, response=None): 68 | self.message = self.base.format(entity=entity) 69 | self.response = response 70 | if response: 71 | self.message = ( 72 | f"{self.message} Error: " 73 | f"{response}" 74 | ) 75 | 76 | 77 | class PowerFlexFailDeleting(PowerFlexClientException): 78 | """ 79 | Exception raised when deleting a PowerFlex entity fails. 80 | """ 81 | base = 'Failed to delete PowerFlex {entity} with id {_id}.' 82 | 83 | def __init__(self, entity, entity_id, response=None): 84 | self.message = self.base.format(entity=entity, _id=entity_id) 85 | self.response = response 86 | if response: 87 | self.message = f"{self.message} Error: {response}" 88 | 89 | 90 | class PowerFlexFailQuerying(PowerFlexClientException): 91 | """ 92 | Exception raised when querying a PowerFlex entity fails. 93 | """ 94 | base = 'Failed to query PowerFlex {entity}' 95 | 96 | def __init__(self, entity, entity_id=None, response=None): 97 | base = self.base.format(entity=entity) 98 | self.response = response 99 | if entity_id and response is None: 100 | self.message = f"{base} with id {entity_id}." 101 | elif entity is None and response: 102 | self.message = f"{base} Error: {response}." 103 | elif entity and response: 104 | self.message = f"{base} with id {entity_id}. Error: {response}." 105 | else: 106 | self.message = f"{base}." 107 | 108 | 109 | class PowerFlexFailRenaming(PowerFlexClientException): 110 | """ 111 | Exception raised when renaming a PowerFlex entity fails. 112 | """ 113 | base = 'Failed to rename PowerFlex {entity} with id {_id}.' 114 | 115 | def __init__(self, entity, entity_id, response=None): 116 | self.message = self.base.format(entity=entity, _id=entity_id) 117 | self.response = response 118 | if response: 119 | self.message = f"{self.message} Error: {response}" 120 | 121 | class PowerFlexFailEntityOperation(PowerFlexClientException): 122 | """ 123 | Exception raised when performing an operation on a PowerFlex entity fails. 124 | """ 125 | base = 'Failed to perform {action} on PowerFlex {entity} with id {_id}.' 126 | 127 | def __init__(self, entity, entity_id, action, response=None): 128 | self.message = \ 129 | self.base.format(action=action, entity=entity, _id=entity_id) 130 | self.response = response 131 | if response: 132 | self.message = f"{self.message} Error: {response}" 133 | -------------------------------------------------------------------------------- /PyPowerFlex/objects/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """This module contains the objects for interacting with the PowerFlex APIs.""" 17 | 18 | from PyPowerFlex.objects.device import Device 19 | from PyPowerFlex.objects.fault_set import FaultSet 20 | from PyPowerFlex.objects.protection_domain import ProtectionDomain 21 | from PyPowerFlex.objects.sdc import Sdc 22 | from PyPowerFlex.objects.sds import Sds 23 | from PyPowerFlex.objects.sdt import Sdt 24 | from PyPowerFlex.objects.snapshot_policy import SnapshotPolicy 25 | from PyPowerFlex.objects.storage_pool import StoragePool 26 | from PyPowerFlex.objects.acceleration_pool import AccelerationPool 27 | from PyPowerFlex.objects.system import System 28 | from PyPowerFlex.objects.volume import Volume 29 | from PyPowerFlex.objects.utility import PowerFlexUtility 30 | from PyPowerFlex.objects.replication_consistency_group import ReplicationConsistencyGroup 31 | from PyPowerFlex.objects.replication_pair import ReplicationPair 32 | from PyPowerFlex.objects.service_template import ServiceTemplate 33 | from PyPowerFlex.objects.managed_device import ManagedDevice 34 | from PyPowerFlex.objects.deployment import Deployment 35 | from PyPowerFlex.objects.firmware_repository import FirmwareRepository 36 | from PyPowerFlex.objects.host import Host 37 | 38 | 39 | __all__ = [ 40 | 'Device', 41 | 'FaultSet', 42 | 'ProtectionDomain', 43 | 'Sdc', 44 | 'Sds', 45 | 'Sdt', 46 | 'SnapshotPolicy', 47 | 'StoragePool', 48 | 'AccelerationPool', 49 | 'System', 50 | 'Volume', 51 | 'PowerFlexUtility', 52 | 'ReplicationConsistencyGroup', 53 | 'ReplicationPair', 54 | 'ServiceTemplate', 55 | 'ManagedDevice', 56 | 'Deployment', 57 | 'FirmwareRepository', 58 | 'Host', 59 | ] 60 | -------------------------------------------------------------------------------- /PyPowerFlex/objects/acceleration_pool.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for interacting with accelaration pool APIs.""" 17 | 18 | # pylint: disable=too-few-public-methods,duplicate-code 19 | 20 | import logging 21 | from PyPowerFlex import base_client 22 | from PyPowerFlex import exceptions 23 | 24 | 25 | LOG = logging.getLogger(__name__) 26 | 27 | 28 | class MediaType: 29 | """Acceleration pool media types.""" 30 | 31 | ssd = 'SSD' 32 | nvdimm = 'NVDIMM' 33 | 34 | 35 | class AccelerationPool(base_client.EntityRequest): 36 | """ 37 | A class representing a PowerFlex acceleration pool. 38 | 39 | This class provides methods to create, delete, and query acceleration pools. 40 | """ 41 | def create(self, 42 | media_type, 43 | protection_domain_id, 44 | name=None, 45 | is_rfcache=None): 46 | """Create PowerFlex acceleration pool. 47 | 48 | :param media_type: one of predefined attributes of MediaType 49 | :type media_type: str 50 | :type protection_domain_id: str 51 | :type name: str 52 | :type is_rfcache: bool 53 | :rtype: dict 54 | """ 55 | 56 | if media_type == MediaType.ssd and not is_rfcache: 57 | msg = 'is_rfcache must be set for media_type SSD.' 58 | raise exceptions.InvalidInput(msg) 59 | params = { 60 | 'mediaType': media_type, 61 | 'protectionDomainId': protection_domain_id, 62 | 'name': name, 63 | 'isRfcache': is_rfcache 64 | } 65 | 66 | return self._create_entity(params) 67 | 68 | def delete(self, acceleration_pool_id): 69 | """Delete PowerFlex acceleration pool. 70 | 71 | :type acceleration_pool_id: str 72 | :rtype: None 73 | """ 74 | 75 | return self._delete_entity(acceleration_pool_id) 76 | 77 | def query_selected_statistics(self, properties, ids=None): 78 | """Query PowerFlex acceleration pool statistics. 79 | 80 | :type properties: list 81 | :type ids: list of acceleration pool IDs or None for all acceleration 82 | pools 83 | :rtype: dict 84 | """ 85 | 86 | action = "querySelectedStatistics" 87 | 88 | params = {'properties': properties} 89 | 90 | if ids: 91 | params["ids"] = ids 92 | else: 93 | params["allIds"] = "" 94 | 95 | return self._query_selected_statistics(action, params) 96 | -------------------------------------------------------------------------------- /PyPowerFlex/objects/deployment.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for doing the deployment.""" 17 | 18 | # pylint: disable=arguments-renamed,too-many-arguments,too-many-positional-arguments,no-member 19 | 20 | import logging 21 | import requests 22 | from PyPowerFlex import base_client 23 | from PyPowerFlex import exceptions 24 | from PyPowerFlex import utils 25 | LOG = logging.getLogger(__name__) 26 | 27 | 28 | class Deployment(base_client.EntityRequest): 29 | """ 30 | A class representing Deployment client. 31 | """ 32 | def get( 33 | self, 34 | filters=None, 35 | full=None, 36 | include_devices=None, 37 | include_template=None, 38 | limit=None, 39 | offset=None, 40 | sort=None): 41 | """ 42 | Retrieve all Deployments with filter, sort, pagination 43 | :param filters: (Optional) The filters to apply to the results. 44 | :param full: (Optional) Whether to return full details for each result. 45 | :param include_devices: (Optional) Whether to include devices in the response. 46 | :param include_template: (Optional) Whether to include service templates in the response. 47 | :param limit: (Optional) Page limit. 48 | :param offset: (Optional) Pagination offset. 49 | :param sort: (Optional) The field to sort the results by. 50 | :return: A list of dictionary containing the retrieved Deployments. 51 | """ 52 | params = { 53 | 'filter': filters, 54 | 'full': full, 55 | 'sort': sort, 56 | 'offset': offset, 57 | 'limit': limit, 58 | 'includeDevices': include_devices, 59 | 'includeTemplate': include_template 60 | } 61 | r, response = self.send_get_request( 62 | utils.build_uri_with_params( 63 | self.deployment_url, **params)) 64 | if r.status_code != requests.codes.ok: 65 | msg = f'Failed to retrieve deployments. Error: {response}' 66 | LOG.error(msg) 67 | raise exceptions.PowerFlexClientException(msg) 68 | return response 69 | 70 | def get_by_id(self, deployment_id): 71 | """ 72 | Retrieve Deployment for specified ID. 73 | :param deployment_id: Deployment ID. 74 | :return: A dictionary containing the retrieved Deployment. 75 | """ 76 | r, response = self.send_get_request( 77 | f'{self.deployment_url}/{deployment_id}') 78 | if r.status_code != requests.codes.ok: 79 | msg = ( 80 | f'Failed to retrieve deployment by id {deployment_id}. Error: {response}') 81 | LOG.error(msg) 82 | raise exceptions.PowerFlexClientException(msg) 83 | return response 84 | 85 | def validate(self, rg_data): 86 | """ 87 | Validates a new deployment. 88 | Args: 89 | rg_data (dict): The resource group data to be deployed. 90 | Returns: 91 | dict: The response from the deployment API. 92 | Raises: 93 | PowerFlexClientException: If the deployment fails. 94 | """ 95 | r, response = self.send_post_request( 96 | f'{self.deployment_url}/validate', rg_data) 97 | if r.status_code != requests.codes.ok: 98 | msg = f'Failed to validate the deployment. Error: {response}' 99 | LOG.error(msg) 100 | raise exceptions.PowerFlexClientException(msg) 101 | 102 | return response 103 | 104 | def create(self, rg_data): 105 | """ 106 | Creates a new deployment. 107 | Args: 108 | rg_data (dict): The resource group data to be deployed. 109 | Returns: 110 | dict: The response from the deployment API. 111 | Raises: 112 | PowerFlexClientException: If the deployment fails. 113 | """ 114 | r, response = self.send_post_request(self.deployment_url, rg_data) 115 | if r.status_code != requests.codes.ok: 116 | msg = f'Failed to create a new deployment. Error: {response}' 117 | LOG.error(msg) 118 | raise exceptions.PowerFlexClientException(msg) 119 | 120 | return response 121 | 122 | def edit(self, deployment_id, rg_data): 123 | """ 124 | Edit a deployment with the given ID using the provided data. 125 | Args: 126 | deployment_id (str): The ID of the deployment to edit. 127 | rg_data (dict): The data to use for editing the deployment. 128 | Returns: 129 | dict: The response from the API. 130 | Raises: 131 | PowerFlexClientException: If the request fails. 132 | """ 133 | request_url = f'{self.deployment_url}/{deployment_id}' 134 | r, response = self.send_put_request(request_url, rg_data) 135 | 136 | if r.status_code != requests.codes.ok: 137 | msg = f'Failed to edit the deployment. Error: {response}' 138 | LOG.error(msg) 139 | raise exceptions.PowerFlexClientException(msg) 140 | 141 | return response 142 | 143 | def delete(self, deployment_id): 144 | """ 145 | Deletes a deployment with the given ID. 146 | Args: 147 | deployment_id (str): The ID of the deployment to delete. 148 | Returns: 149 | str: The response from the delete request. 150 | Raises: 151 | exceptions.PowerFlexClientException: If the delete request fails. 152 | """ 153 | request_url = f'{self.deployment_url}/{deployment_id}' 154 | response = self.send_delete_request(request_url) 155 | 156 | if response.status_code != requests.codes.no_content: 157 | msg = f'Failed to delete deployment. Error: {response}' 158 | LOG.error(msg) 159 | raise exceptions.PowerFlexClientException(msg) 160 | 161 | return response 162 | -------------------------------------------------------------------------------- /PyPowerFlex/objects/device.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for interacting with device APIs.""" 17 | 18 | # pylint: disable=too-few-public-methods,too-many-arguments,too-many-positional-arguments,no-member,duplicate-code 19 | 20 | import logging 21 | 22 | import requests 23 | 24 | from PyPowerFlex import base_client 25 | from PyPowerFlex import exceptions 26 | 27 | 28 | LOG = logging.getLogger(__name__) 29 | 30 | 31 | class MediaType: 32 | """Device media types.""" 33 | 34 | hdd = 'HDD' 35 | ssd = 'SSD' 36 | nvdimm = 'NVDIMM' 37 | 38 | 39 | class ExternalAccelerationType: 40 | """Device external acceleration types.""" 41 | 42 | invalid = 'Invalid' 43 | none = 'None' 44 | read = 'Read' 45 | write = 'Write' 46 | read_and_write = 'ReadAndWrite' 47 | 48 | 49 | class Device(base_client.EntityRequest): 50 | """ 51 | A class representing Device client. 52 | """ 53 | def create(self, 54 | current_pathname, 55 | sds_id, 56 | acceleration_pool_id=None, 57 | external_acceleration_type=None, 58 | force=None, 59 | media_type=None, 60 | name=None, 61 | storage_pool_id=None): 62 | """Create PowerFlex device. 63 | 64 | :type current_pathname: str 65 | :type sds_id: str 66 | :type acceleration_pool_id: str 67 | :param external_acceleration_type: one of predefined attributes of 68 | ExternalAccelerationType 69 | :type external_acceleration_type: str 70 | :type force: bool 71 | :param media_type: one of predefined attributes of MediaType 72 | :type media_type: str 73 | :type name: str 74 | :type storage_pool_id: str 75 | :rtype: dict 76 | """ 77 | 78 | if ( 79 | all([storage_pool_id, acceleration_pool_id]) or 80 | not any([storage_pool_id, acceleration_pool_id]) 81 | ): 82 | msg = 'Either storage_pool_id or acceleration_pool_id must be ' \ 83 | 'set.' 84 | raise exceptions.InvalidInput(msg) 85 | 86 | params = { 87 | "deviceCurrentPathname": current_pathname, 88 | "sdsId": sds_id, 89 | "accelerationPoolId": acceleration_pool_id, 90 | "externalAccelerationType": external_acceleration_type, 91 | "forceDeviceTakeover": force, 92 | "mediaType": media_type, 93 | "name": name, 94 | "storagePoolId": storage_pool_id 95 | } 96 | 97 | return self._create_entity(params) 98 | 99 | def delete(self, device_id, force=None): 100 | """Remove PowerFlex device. 101 | 102 | :type device_id: str 103 | :type force: bool 104 | :rtype: None 105 | """ 106 | 107 | params = { 108 | "forceRemove": force 109 | } 110 | 111 | return self._delete_entity(device_id, params) 112 | 113 | def rename(self, device_id, name): 114 | """Rename PowerFlex device. 115 | 116 | :type device_id: str 117 | :type name: str 118 | :rtype: dict 119 | """ 120 | 121 | action = 'setDeviceName' 122 | 123 | params = { 124 | "newName": name 125 | } 126 | 127 | return self._rename_entity(action, device_id, params) 128 | 129 | def set_media_type(self, 130 | device_id, 131 | media_type): 132 | """Set PowerFlex device media type. 133 | 134 | :type device_id: str 135 | :param media_type: one of predefined attributes of MediaType 136 | :type media_type: str 137 | :rtype: dict 138 | """ 139 | 140 | action = 'setMediaType' 141 | 142 | params = {"mediaType": media_type} 143 | 144 | r, response = self.send_post_request(self.base_action_url, 145 | action=action, 146 | entity=self.entity, 147 | entity_id=device_id, 148 | params=params) 149 | if r.status_code != requests.codes.ok: 150 | msg = ( 151 | f"Failed to set media type for PowerFlex {self.entity} " 152 | f"with id {device_id}. Error: {response}" 153 | ) 154 | LOG.error(msg) 155 | raise exceptions.PowerFlexClientException(msg) 156 | 157 | return self.get(entity_id=device_id) 158 | 159 | def query_selected_statistics(self, properties, ids=None): 160 | """Query PowerFlex device statistics. 161 | 162 | :type properties: list 163 | :type ids: list of device IDs or None for all devices 164 | :rtype: dict 165 | """ 166 | 167 | action = "querySelectedStatistics" 168 | 169 | params = {'properties': properties} 170 | 171 | if ids: 172 | params["ids"] = ids 173 | else: 174 | params["allIds"] = "" 175 | 176 | return self._query_selected_statistics(action, params) 177 | -------------------------------------------------------------------------------- /PyPowerFlex/objects/fault_set.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for interacting with fault set APIs.""" 17 | 18 | # pylint: disable=no-member,duplicate-code 19 | 20 | import logging 21 | 22 | import requests 23 | 24 | from PyPowerFlex import base_client 25 | from PyPowerFlex import exceptions 26 | 27 | 28 | LOG = logging.getLogger(__name__) 29 | 30 | 31 | class FaultSet(base_client.EntityRequest): 32 | """ 33 | A class representing Fault Set client. 34 | """ 35 | def clear(self, fault_set_id): 36 | """Clear PowerFlex fault set. 37 | 38 | :type fault_set_id: str 39 | :rtype: dict 40 | """ 41 | 42 | action = 'clearFaultSet' 43 | 44 | r, response = self.send_post_request(self.base_action_url, 45 | action=action, 46 | entity=self.entity, 47 | entity_id=fault_set_id) 48 | if r.status_code != requests.codes.ok: 49 | msg = ( 50 | f"Failed to clear PowerFlex {self.entity} " 51 | f"with id {fault_set_id}. Error: {response}" 52 | ) 53 | LOG.error(msg) 54 | raise exceptions.PowerFlexClientException(msg) 55 | 56 | return self.get(entity_id=fault_set_id) 57 | 58 | def create(self, protection_domain_id, name=None): 59 | """Create PowerFlex fault set. 60 | 61 | :type protection_domain_id: str 62 | :type name: str 63 | :rtype: dict 64 | """ 65 | 66 | params = { 67 | "protectionDomainId": protection_domain_id, 68 | "name": name 69 | } 70 | 71 | return self._create_entity(params) 72 | 73 | def get_sdss(self, fault_set_id, filter_fields=None, fields=None): 74 | """Get related PowerFlex SDSs for fault set. 75 | 76 | :type fault_set_id: str 77 | :type filter_fields: dict 78 | :type fields: list|tuple 79 | :rtype: list[dict] 80 | """ 81 | 82 | return self.get_related(fault_set_id, 83 | 'Sds', 84 | filter_fields, 85 | fields) 86 | 87 | def delete(self, fault_set_id): 88 | """Remove PowerFlex fault set. 89 | 90 | :type fault_set_id: str 91 | :rtype: None 92 | """ 93 | 94 | return self._delete_entity(fault_set_id) 95 | 96 | def rename(self, fault_set_id, name): 97 | """Rename PowerFlex fault set. 98 | 99 | :type fault_set_id: str 100 | :type name: str 101 | :rtype: dict 102 | """ 103 | 104 | action = 'setFaultSetName' 105 | 106 | params = { 107 | "newName": name 108 | } 109 | 110 | return self._rename_entity(action, fault_set_id, params) 111 | 112 | def query_selected_statistics(self, properties, ids=None): 113 | """Query PowerFlex fault set statistics. 114 | 115 | :type properties: list 116 | :type ids: list of fault set IDs or None for all fault sets 117 | :rtype: dict 118 | """ 119 | 120 | action = "querySelectedStatistics" 121 | 122 | params = {'properties': properties} 123 | 124 | if ids: 125 | params["ids"] = ids 126 | else: 127 | params["allIds"] = "" 128 | 129 | return self._query_selected_statistics(action, params) 130 | -------------------------------------------------------------------------------- /PyPowerFlex/objects/firmware_repository.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for interacting with firmware repository APIs.""" 17 | 18 | # pylint: disable=arguments-renamed,no-member,too-many-arguments,too-many-positional-arguments 19 | 20 | import logging 21 | import requests 22 | from PyPowerFlex import base_client 23 | from PyPowerFlex import exceptions 24 | from PyPowerFlex import utils 25 | LOG = logging.getLogger(__name__) 26 | 27 | 28 | class FirmwareRepository(base_client.EntityRequest): 29 | """ 30 | A class representing Firmware Repository client. 31 | """ 32 | def get(self, filters=None, limit=None, offset=None, sort=None, 33 | related=False, bundles=False, components=False): 34 | """ 35 | Retrieve all firmware repository with filter, sort, pagination 36 | :param filters: (Optional) The filters to apply to the results. 37 | :param limit: (Optional) Page limit. 38 | :param offset: (Optional) Pagination offset. 39 | :param sort: (Optional) The field to sort the results by. 40 | :param related: Whether to include related entities in the response. 41 | :param bundles: Whether to include bundles in the response. 42 | :param components: Whether to include components in the response. 43 | :return: A list of dictionary containing the retrieved firmware repository. 44 | """ 45 | params = { 46 | 'filter': filters, 47 | 'sort': sort, 48 | 'offset': offset, 49 | 'limit': limit, 50 | 'related': related, 51 | 'bundles': bundles, 52 | 'components': components 53 | } 54 | r, response = self.send_get_request( 55 | utils.build_uri_with_params( 56 | self.firmware_repository_url, **params)) 57 | if r.status_code != requests.codes.ok: 58 | msg = ( 59 | f'Failed to retrieve firmware repository. Error: {response}') 60 | LOG.error(msg) 61 | raise exceptions.PowerFlexClientException(msg) 62 | return response 63 | -------------------------------------------------------------------------------- /PyPowerFlex/objects/host.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for interacting with host APIs.""" 17 | 18 | import logging 19 | from PyPowerFlex import base_client 20 | 21 | 22 | LOG = logging.getLogger(__name__) 23 | 24 | class Host(base_client.EntityRequest): 25 | """ 26 | A class representing Host client. 27 | """ 28 | def create(self, 29 | nqn, 30 | name=None, 31 | max_num_paths=None, 32 | max_num_sys_ports=None): 33 | """Create a new NVMe host. 34 | 35 | :param nqn: NQN of the NVMe host 36 | :type nqn: str 37 | :param name: Name of the NVMe Host 38 | :type name: str 39 | :param maxNumPaths: Maximum Number of Paths Per Volume. 40 | :type maxNumPaths: str 41 | :param maxNumSysPorts: Maximum Number of Ports Per Protection Domain 42 | :type maxNumSysPorts: str 43 | :return: Created host 44 | :rtype: dict 45 | """ 46 | 47 | params = { 48 | "nqn": nqn, 49 | "name": name, 50 | "maxNumPaths": max_num_paths, 51 | "maxNumSysPorts": max_num_sys_ports 52 | } 53 | 54 | return self._create_entity(params) 55 | 56 | def modify_max_num_paths(self, host_id, max_num_paths): 57 | """Modify Maximum Number of Paths Per Volume. 58 | 59 | :param host_id: ID of the SDC 60 | :type host_id: str 61 | :param max_num_paths: Maximum Number of Paths Per Volume. 62 | :type max_num_paths: str 63 | :return: result 64 | :rtype: dict 65 | """ 66 | 67 | action = 'modifyMaxNumPaths' 68 | 69 | params = {"newMaxNumPaths": max_num_paths} 70 | 71 | return self._perform_entity_operation_based_on_action( 72 | action=action, entity_id=host_id, params=params, add_entity=False) 73 | 74 | def modify_max_num_sys_ports(self, host_id, max_num_sys_ports): 75 | """Modify Maximum Number of Ports Per Protection Domain. 76 | 77 | :param host_id: ID of the SDC 78 | :type host_id: str 79 | :param max_num_sys_ports: Maximum Number of Ports Per Protection Domain. 80 | :type max_num_sys_ports: str 81 | :return: result 82 | :rtype: dict 83 | """ 84 | 85 | action = 'modifyMaxNumSysPorts' 86 | 87 | params = {"newMaxNumSysPorts": max_num_sys_ports} 88 | 89 | return self._perform_entity_operation_based_on_action( 90 | action=action, entity_id=host_id, params=params, add_entity=False) 91 | -------------------------------------------------------------------------------- /PyPowerFlex/objects/managed_device.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for interacting with managed device APIs.""" 17 | 18 | # pylint: disable=arguments-renamed,no-member 19 | 20 | import logging 21 | import requests 22 | from PyPowerFlex import base_client 23 | from PyPowerFlex import exceptions 24 | from PyPowerFlex import utils 25 | LOG = logging.getLogger(__name__) 26 | 27 | 28 | class ManagedDevice(base_client.EntityRequest): 29 | """ 30 | A class representing Managed Device client. 31 | """ 32 | def get(self, filters=None, limit=None, offset=None, sort=None): 33 | """ 34 | Retrieve all devices from inventory with filter, sort, pagination 35 | :param filters: (Optional) The filters to apply to the results. 36 | :param limit: (Optional) Page limit. 37 | :param offset: (Optional) Pagination offset. 38 | :param sort: (Optional) The field to sort the results by. 39 | :return: A list of dictionary containing the retrieved devices from inventory. 40 | """ 41 | params = { 42 | "filter": filters, 43 | "limit": limit, 44 | "offset": offset, 45 | "sort": sort 46 | } 47 | r, response = self.send_get_request( 48 | utils.build_uri_with_params( 49 | self.managed_device_url, **params)) 50 | if r.status_code != requests.codes.ok: 51 | msg = f'Failed to retrieve managed devices. Error: {response}' 52 | LOG.error(msg) 53 | raise exceptions.PowerFlexClientException(msg) 54 | return response 55 | -------------------------------------------------------------------------------- /PyPowerFlex/objects/protection_domain.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for interacting with protection domain APIs.""" 17 | 18 | # pylint: disable=too-few-public-methods,no-member,too-many-arguments,too-many-positional-arguments,duplicate-code 19 | 20 | import logging 21 | 22 | import requests 23 | 24 | from PyPowerFlex import base_client 25 | from PyPowerFlex import exceptions 26 | 27 | 28 | LOG = logging.getLogger(__name__) 29 | 30 | 31 | class RFCacheOperationMode: 32 | """RFcache operation mode.""" 33 | 34 | none = 'None' 35 | read = 'Read' 36 | write = 'Write' 37 | read_and_write = 'ReadAndWrite' 38 | write_miss = 'WriteMiss' 39 | 40 | 41 | class ProtectionDomain(base_client.EntityRequest): 42 | """ 43 | A class representing Protection Domain client. 44 | """ 45 | def activate(self, protection_domain_id, force=False): 46 | """Activate PowerFlex protection domain. 47 | 48 | :type protection_domain_id: str 49 | :type force: bool 50 | :rtype: dict 51 | """ 52 | 53 | action = 'activateProtectionDomain' 54 | 55 | params = { 56 | "forceActivate": force 57 | } 58 | 59 | r, response = self.send_post_request(self.base_action_url, 60 | action=action, 61 | entity=self.entity, 62 | entity_id=protection_domain_id, 63 | params=params) 64 | if r.status_code != requests.codes.ok: 65 | msg = ( 66 | f"Failed to activate PowerFlex {self.entity} " 67 | f"with id {protection_domain_id}. Error: {response}" 68 | ) 69 | LOG.error(msg) 70 | raise exceptions.PowerFlexClientException(msg) 71 | 72 | return self.get(entity_id=protection_domain_id) 73 | 74 | def create(self, name): 75 | """Create PowerFlex protection domain. 76 | 77 | :type name: str 78 | :rtype: dict 79 | """ 80 | 81 | params = {"name": name} 82 | 83 | return self._create_entity(params) 84 | 85 | def get_sdss(self, protection_domain_id, filter_fields=None, fields=None): 86 | """Get related PowerFlex SDSs for protection domain. 87 | 88 | :type protection_domain_id: str 89 | :type filter_fields: dict 90 | :type fields: list|tuple 91 | :rtype: list[dict] 92 | """ 93 | 94 | return self.get_related(protection_domain_id, 95 | 'Sds', 96 | filter_fields, 97 | fields) 98 | 99 | def get_storage_pools(self, 100 | protection_domain_id, 101 | filter_fields=None, 102 | fields=None): 103 | """Get related PowerFlex storage pools for protection domain. 104 | 105 | :type protection_domain_id: str 106 | :type filter_fields: dict 107 | :type fields: list|tuple 108 | :rtype: list[dict] 109 | """ 110 | 111 | return self.get_related(protection_domain_id, 112 | 'StoragePool', 113 | filter_fields, 114 | fields) 115 | 116 | def delete(self, protection_domain_id): 117 | """Remove PowerFlex protection domain. 118 | 119 | :type protection_domain_id: str 120 | :rtype: None 121 | """ 122 | 123 | return self._delete_entity(protection_domain_id) 124 | 125 | def inactivate(self, protection_domain_id, force=False): 126 | """Inactivate PowerFlex protection domain. 127 | 128 | :type protection_domain_id: str 129 | :type force: bool 130 | :rtype: dict 131 | """ 132 | 133 | action = 'inactivateProtectionDomain' 134 | 135 | params = { 136 | "forceShutdown": force 137 | } 138 | 139 | r, response = self.send_post_request(self.base_action_url, 140 | action=action, 141 | entity=self.entity, 142 | entity_id=protection_domain_id, 143 | params=params) 144 | if r.status_code != requests.codes.ok: 145 | msg = ( 146 | f"Failed to inactivate PowerFlex {self.entity} " 147 | f"with id {protection_domain_id}. Error: {response}" 148 | ) 149 | LOG.error(msg) 150 | raise exceptions.PowerFlexClientException(msg) 151 | 152 | return self.get(entity_id=protection_domain_id) 153 | 154 | def rename(self, protection_domain_id, name): 155 | """Rename PowerFlex protection domain. 156 | 157 | :type protection_domain_id: str 158 | :type name: str 159 | :rtype: dict 160 | """ 161 | 162 | action = 'setProtectionDomainName' 163 | 164 | params = {"name": name} 165 | 166 | return self._rename_entity(action, protection_domain_id, params) 167 | 168 | def network_limits(self, protection_domain_id, rebuild_limit=None, 169 | rebalance_limit=None, vtree_migration_limit=None, 170 | overall_limit=None): 171 | """ 172 | Setting the Network limits of the protection domain. 173 | 174 | :type protection_domain_id: str 175 | :type rebuild_limit: int 176 | :type rebalance_limit: int 177 | :type vtree_migration_limit: int 178 | :type overall_limit: int 179 | :rtype dict 180 | """ 181 | 182 | action = "setSdsNetworkLimits" 183 | 184 | params = { 185 | "rebuildLimitInKbps": rebuild_limit, 186 | "rebalanceLimitInKbps": rebalance_limit, 187 | "vtreeMigrationLimitInKbps": vtree_migration_limit, 188 | "overallLimitInKbps": overall_limit 189 | } 190 | r, response = self.send_post_request(self.base_action_url, 191 | action=action, 192 | entity=self.entity, 193 | entity_id=protection_domain_id, 194 | params=params) 195 | 196 | if r.status_code != requests.codes.ok: 197 | msg = ( 198 | f"Failed to update the network limits of PowerFlex {self.entity} " 199 | f"with id {protection_domain_id}. Error: {response}" 200 | ) 201 | LOG.error(msg) 202 | raise exceptions.PowerFlexClientException(msg) 203 | 204 | return self.get(entity_id=protection_domain_id) 205 | 206 | def set_rfcache_enabled(self, protection_domain_id, enable_rfcache=None): 207 | """ 208 | Enable/Disable the RFcache in the Protection Domain. 209 | 210 | :type protection_domain_id: str 211 | :type enable_rfcache: bool 212 | :rtype dict 213 | """ 214 | 215 | action = "disableSdsRfcache" 216 | if enable_rfcache: 217 | action = "enableSdsRfcache" 218 | 219 | r, response = self.send_post_request(self.base_action_url, 220 | action=action, 221 | entity=self.entity, 222 | entity_id=protection_domain_id) 223 | if r.status_code != requests.codes.ok: 224 | msg = ( 225 | f"Failed to enable/disable RFcache in PowerFlex {self.entity} " 226 | f"with id {protection_domain_id}. Error: {response}" 227 | ) 228 | LOG.error(msg) 229 | raise exceptions.PowerFlexClientException(msg) 230 | 231 | return self.get(entity_id=protection_domain_id) 232 | 233 | def rfcache_parameters(self, protection_domain_id, page_size=None, 234 | max_io_limit=None, pass_through_mode=None): 235 | """ 236 | Set RF cache parameters of the protection domain. 237 | 238 | :type protection_domain_id: str 239 | :type page_size: int 240 | :type max_io_limit: int 241 | :type pass_through_mode: str 242 | :rtype dict 243 | """ 244 | 245 | action = "setRfcacheParameters" 246 | 247 | params = { 248 | "pageSizeKb": page_size, 249 | "maxIOSizeKb": max_io_limit, 250 | "rfcacheOperationMode": pass_through_mode 251 | } 252 | 253 | r, response = self.send_post_request(self.base_action_url, 254 | action=action, 255 | entity=self.entity, 256 | entity_id=protection_domain_id, 257 | params=params) 258 | 259 | if r.status_code != requests.codes.ok: 260 | msg = ( 261 | f"Failed to set RFcache parameters in PowerFlex {self.entity} " 262 | f"with id {protection_domain_id}. Error: {response}" 263 | ) 264 | LOG.error(msg) 265 | raise exceptions.PowerFlexClientException(msg) 266 | 267 | return self.get(entity_id=protection_domain_id) 268 | 269 | def query_selected_statistics(self, properties, ids=None): 270 | """Query PowerFlex protection domain statistics. 271 | 272 | :type properties: list 273 | :type ids: list of protection domain IDs or None for all protection 274 | domains 275 | :rtype: dict 276 | """ 277 | 278 | action = "querySelectedStatistics" 279 | 280 | params = {'properties': properties} 281 | 282 | if ids: 283 | params["ids"] = ids 284 | else: 285 | params["allIds"] = "" 286 | 287 | return self._query_selected_statistics(action, params) 288 | -------------------------------------------------------------------------------- /PyPowerFlex/objects/replication_consistency_group.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for interacting with replication consistency group APIs.""" 17 | 18 | # pylint: disable=too-many-public-methods,no-member,too-many-arguments,too-many-positional-arguments,duplicate-code 19 | 20 | import logging 21 | 22 | import requests 23 | 24 | from PyPowerFlex import base_client 25 | from PyPowerFlex import exceptions 26 | from PyPowerFlex.constants import RCGConstants 27 | 28 | 29 | LOG = logging.getLogger(__name__) 30 | 31 | 32 | class ReplicationConsistencyGroup(base_client.EntityRequest): 33 | """ 34 | A class representing Replication Consistency Group client. 35 | """ 36 | def create_snapshot(self, 37 | rcg_id): 38 | """Create a snapshot of PowerFlex replication consistency group. 39 | 40 | :param rcg_id: str 41 | :return: dict 42 | """ 43 | 44 | action = 'createReplicationConsistencyGroupSnapshots' 45 | 46 | r, response = self.send_post_request(self.base_action_url, 47 | action=action, 48 | entity=self.entity, 49 | entity_id=rcg_id) 50 | if r.status_code != requests.codes.ok: 51 | msg = ( 52 | f"Failed to create a snapshot of PowerFlex {self.entity} " 53 | f"with id {rcg_id}. Error: {response}" 54 | ) 55 | LOG.error(msg) 56 | raise exceptions.PowerFlexClientException(msg) 57 | 58 | return self.get(entity_id=rcg_id) 59 | 60 | def get_statistics(self, rcg_id): 61 | """Get related PowerFlex Statistics for RCG. 62 | 63 | :type rcg_id: str 64 | :rtype: dict 65 | """ 66 | 67 | return self.get_related(rcg_id, 68 | 'Statistics') 69 | 70 | def create(self, 71 | rpo, 72 | protection_domain_id, 73 | remote_protection_domain_id=None, 74 | peer_mdm_id=None, 75 | destination_system_id=None, 76 | name=None, 77 | force_ignore_consistency=None, 78 | activity_mode=None): 79 | """Create PowerFlex RCG. 80 | 81 | :param rpo: int 82 | :param protection_domain_id: str 83 | :param remote_protection_domain_id: str 84 | :param peer_mdm_id: str 85 | :type destination_system_id: str 86 | :param name: str 87 | :param force_ignore_consistency: bool 88 | :type activity_mode: str 89 | :return: dict 90 | """ 91 | 92 | params = { 93 | "rpoInSeconds": rpo, 94 | "protectionDomainId": protection_domain_id, 95 | "remoteProtectionDomainId": remote_protection_domain_id, 96 | "peerMdmId": peer_mdm_id, 97 | "destinationSystemId": destination_system_id, 98 | "name": name, 99 | "forceIgnoreConsistency": force_ignore_consistency, 100 | "activityMode": activity_mode 101 | } 102 | 103 | return self._create_entity(params) 104 | 105 | def delete(self, 106 | rcg_id, 107 | force_ignore_consistency=None): 108 | """Delete PowerFlex RCG. 109 | 110 | :param rcg_id: str 111 | :param force_ignore_consistency: bool 112 | :return: None 113 | """ 114 | 115 | params = {"forceIgnoreConsistency": force_ignore_consistency} 116 | 117 | return self._delete_entity(rcg_id, params) 118 | 119 | def activate(self, rcg_id): 120 | """Activate PowerFlex RCG. 121 | 122 | :param rcg_id: str 123 | :return: dict 124 | """ 125 | action = f"activate{self.entity}" 126 | return self._perform_entity_operation_based_on_action( 127 | rcg_id, action, add_entity=False) 128 | 129 | def inactivate(self, rcg_id): 130 | """Inactivate PowerFlex RCG. 131 | 132 | :param rcg_id: str 133 | :return: dict 134 | """ 135 | action = f"terminate{self.entity}" 136 | return self._perform_entity_operation_based_on_action( 137 | rcg_id, action, add_entity=False) 138 | 139 | def freeze(self, rcg_id): 140 | """Freeze PowerFlex RCG. 141 | 142 | :param rcg_id: str 143 | :return: dict 144 | """ 145 | 146 | return self._perform_entity_operation_based_on_action( 147 | rcg_id, "freezeApply") 148 | 149 | def unfreeze(self, rcg_id): 150 | """Freeze PowerFlex RCG. 151 | 152 | :param rcg_id: str 153 | :return: dict 154 | """ 155 | 156 | return self._perform_entity_operation_based_on_action( 157 | rcg_id, "unfreezeApply") 158 | 159 | def pause(self, rcg_id, pause_mode): 160 | """Pause PowerFlex RCG. 161 | 162 | :param rcg_id: str 163 | :param pause_mode: str 164 | :return: dict 165 | """ 166 | 167 | params = {"pauseMode": pause_mode} 168 | return self._perform_entity_operation_based_on_action( 169 | rcg_id, "pause", params) 170 | 171 | def resume(self, rcg_id): 172 | """Resume PowerFlex RCG. 173 | 174 | :param rcg_id: str 175 | :return: dict 176 | """ 177 | 178 | return self._perform_entity_operation_based_on_action(rcg_id, "resume") 179 | 180 | def failover(self, rcg_id): 181 | """Failover PowerFlex RCG. 182 | 183 | :param rcg_id: str 184 | :return: dict 185 | """ 186 | 187 | return self._perform_entity_operation_based_on_action( 188 | rcg_id, "failover") 189 | 190 | def sync(self, rcg_id): 191 | """Synchronize PowerFlex RCG. 192 | 193 | :param rcg_id: str 194 | :return: dict 195 | """ 196 | 197 | return self._perform_entity_operation_based_on_action( 198 | rcg_id, "syncNow") 199 | 200 | def restore(self, rcg_id): 201 | """Restore PowerFlex RCG. 202 | 203 | :param rcg_id: str 204 | :return: dict 205 | """ 206 | 207 | return self._perform_entity_operation_based_on_action( 208 | rcg_id, "restore") 209 | 210 | def reverse(self, rcg_id): 211 | """Reverse PowerFlex RCG. 212 | 213 | :param rcg_id: str 214 | :return: dict 215 | """ 216 | 217 | return self._perform_entity_operation_based_on_action( 218 | rcg_id, "reverse") 219 | 220 | def switchover(self, rcg_id, force=False): 221 | """Switch over PowerFlex RCG. 222 | 223 | :param rcg_id: str 224 | :param force: bool 225 | :return: dict 226 | """ 227 | url_params = { 228 | 'force': force 229 | } 230 | return self._perform_entity_operation_based_on_action( 231 | rcg_id, "switchover", **url_params) 232 | 233 | def set_as_consistent(self, rcg_id): 234 | """Set PowerFlex RCG as consistent. 235 | 236 | :param rcg_id: str 237 | :return: dict 238 | """ 239 | action = f"set{self.entity}Consistent" 240 | return self._perform_entity_operation_based_on_action( 241 | rcg_id, action, add_entity=False) 242 | 243 | def set_as_inconsistent(self, rcg_id): 244 | """Set PowerFlex RCG as in-consistent. 245 | 246 | :param rcg_id: str 247 | :return: dict 248 | """ 249 | action = f"set{self.entity}Inconsistent" 250 | return self._perform_entity_operation_based_on_action( 251 | rcg_id, action, add_entity=False) 252 | 253 | def modify_rpo(self, rcg_id, rpo_in_seconds): 254 | """Modify rpo of PowerFlex RCG. 255 | 256 | :param rcg_id: str 257 | :param rpo_in_seconds: int 258 | :return: dict 259 | """ 260 | 261 | params = { 262 | 'rpoInSeconds': rpo_in_seconds 263 | } 264 | action = f"Modify{self.entity}Rpo" 265 | return self._perform_entity_operation_based_on_action( 266 | rcg_id, action, params=params, add_entity=False) 267 | 268 | def modify_target_volume_access_mode( 269 | self, rcg_id, target_volume_access_mode): 270 | """Modify TargetVolumeAccessMode of PowerFlex RCG. 271 | 272 | :param rcg_id: str 273 | :param target_volume_access_mode: str 274 | :return: dict 275 | """ 276 | 277 | params = {"targetVolumeAccessMode": target_volume_access_mode} 278 | action = f"modify{self.entity}TargetVolumeAccessMode" 279 | return self._perform_entity_operation_based_on_action( 280 | rcg_id, action, params=params, add_entity=False) 281 | 282 | def rename_rcg(self, rcg_id, new_name): 283 | """Rename PowerFlex RCG. 284 | 285 | :param rcg_id: str 286 | :param new_name: str 287 | :return: dict 288 | """ 289 | 290 | params = {"newName": new_name} 291 | return self._perform_entity_operation_based_on_action( 292 | rcg_id, "rename", params=params) 293 | 294 | def get_replication_pairs(self, rcg_id): 295 | """Get replication pairs of PowerFlex RCG. 296 | 297 | :param rcg_id: str 298 | :return: dict 299 | """ 300 | 301 | return self.get_related(rcg_id, 302 | 'ReplicationPair') 303 | 304 | def get_all_statistics(self, api_version_less_than_3_6): 305 | """list statistics of all replication consistency groups for PowerFlex. 306 | :param api_version_less_than_3_6: bool 307 | :return: dict 308 | """ 309 | params = {'properties': RCGConstants.DEFAULT_STATISTICS_PROPERTIES} 310 | if not api_version_less_than_3_6: 311 | params = { 312 | 'properties': RCGConstants.DEFAULT_STATISTICS_PROPERTIES_ABOVE_3_5} 313 | params['allIds'] = "" 314 | 315 | r, response = self.send_post_request(self.list_statistics_url, 316 | entity=self.entity, 317 | action="querySelectedStatistics", 318 | params=params) 319 | if r.status_code != requests.codes.ok: 320 | msg = ( 321 | f'Failed to list replication consistency group statistics for PowerFlex. ' 322 | f'Error: {response}' 323 | ) 324 | LOG.error(msg) 325 | raise exceptions.PowerFlexClientException(msg) 326 | 327 | return response 328 | 329 | def query_selected_statistics(self, properties, ids=None): 330 | """Query PowerFlex replication consistency group statistics. 331 | 332 | :type properties: list 333 | :type ids: list of replication consistency group IDs or None for all 334 | replication consistency groups 335 | :rtype: dict 336 | """ 337 | 338 | action = "querySelectedStatistics" 339 | 340 | params = {'properties': properties} 341 | 342 | if ids: 343 | params["ids"] = ids 344 | else: 345 | params["allIds"] = "" 346 | 347 | return self._query_selected_statistics(action, params) 348 | -------------------------------------------------------------------------------- /PyPowerFlex/objects/replication_pair.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for interacting with replication pair APIs.""" 17 | 18 | # pylint: disable=redefined-builtin,no-member,too-many-arguments,too-many-positional-arguments,duplicate-code 19 | 20 | import logging 21 | 22 | import requests 23 | 24 | from PyPowerFlex import base_client 25 | from PyPowerFlex import exceptions 26 | 27 | 28 | LOG = logging.getLogger(__name__) 29 | 30 | 31 | class ReplicationPair(base_client.EntityRequest): 32 | """ 33 | A class representing Replication Pair client. 34 | """ 35 | def get_statistics(self, id): 36 | """Retrieve statistics for the specified ReplicationPair object. 37 | 38 | :type id: str 39 | :rtype: dict 40 | """ 41 | 42 | return self.get_related(id, 43 | 'Statistics') 44 | 45 | def add(self, 46 | source_vol_id, 47 | dest_vol_id, 48 | rcg_id, 49 | copy_type, 50 | name=None): 51 | """Add replication pair to PowerFlex RCG. 52 | 53 | :param source_vol_id: str 54 | :param dest_vol_id: str 55 | :param rcg_id: str 56 | :param copy_type: str 57 | :type name: str 58 | :return: dict 59 | """ 60 | 61 | params = { 62 | "sourceVolumeId": source_vol_id, 63 | "destinationVolumeId": dest_vol_id, 64 | "replicationConsistencyGroupId": rcg_id, 65 | "copyType": copy_type, 66 | "name": name 67 | } 68 | 69 | return self._create_entity(params) 70 | 71 | def remove(self, id): 72 | """Remove replication pair of PowerFlex RCG. 73 | 74 | :param id: str 75 | :return: None 76 | """ 77 | return self._delete_entity(id) 78 | 79 | def pause(self, id): 80 | """Pause the progress of the specified ReplicationPair's initial copy. 81 | 82 | :param id: str 83 | :return: dict 84 | """ 85 | return self._perform_entity_operation_based_on_action( 86 | id, "pausePairInitialCopy", add_entity=False) 87 | 88 | def resume(self, id): 89 | """Resume initial copy of the ReplicationPair. 90 | 91 | :param id: str 92 | :return: dict 93 | """ 94 | return self._perform_entity_operation_based_on_action( 95 | id, "resumePairInitialCopy", add_entity=False) 96 | 97 | def get_all_statistics(self): 98 | """Retrieve statistics for all ReplicationPair objects. 99 | :return: dict 100 | """ 101 | r, response = self.send_post_request(self.list_statistics_url, 102 | entity=self.entity, 103 | action="querySelectedStatistics") 104 | if r.status_code != requests.codes.ok: 105 | msg = ( 106 | 'Failed to list statistics for all ReplicationPair objects. ' 107 | f'Error: {response}' 108 | ) 109 | LOG.error(msg) 110 | raise exceptions.PowerFlexClientException(msg) 111 | 112 | return response 113 | 114 | def query_selected_statistics(self, properties, ids=None): 115 | """Query PowerFlex replication pair statistics. 116 | 117 | :type properties: list 118 | :type ids: list of replication pair IDs or None for all replication 119 | pairs 120 | :rtype: dict 121 | """ 122 | 123 | action = "querySelectedStatistics" 124 | 125 | params = {'properties': properties} 126 | 127 | if ids: 128 | params["ids"] = ids 129 | else: 130 | params["allIds"] = "" 131 | 132 | return self._query_selected_statistics(action, params) 133 | -------------------------------------------------------------------------------- /PyPowerFlex/objects/sdc.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for interacting with SDC APIs.""" 17 | 18 | import logging 19 | from PyPowerFlex import base_client 20 | 21 | 22 | LOG = logging.getLogger(__name__) 23 | 24 | 25 | class Sdc(base_client.EntityRequest): 26 | """ 27 | A class representing SDC client. 28 | """ 29 | def delete(self, sdc_id): 30 | """Remove PowerFlex SDC. 31 | 32 | :type sdc_id: str 33 | :rtype: None 34 | """ 35 | 36 | return self._delete_entity(sdc_id) 37 | 38 | def get_mapped_volumes(self, sdc_id, filter_fields=None, fields=None): 39 | """Get PowerFlex volumes mapped to SDC. 40 | 41 | :type sdc_id: str 42 | :type filter_fields: dict 43 | :type fields: list|tuple 44 | :rtype: list[dict] 45 | """ 46 | 47 | return self.get_related(sdc_id, 'Volume', filter_fields, fields) 48 | 49 | def rename(self, sdc_id, name): 50 | """Rename PowerFlex SDC. 51 | 52 | :type sdc_id: str 53 | :type name: str 54 | :rtype: dict 55 | """ 56 | 57 | action = 'setSdcName' 58 | 59 | params = {"sdcName": name} 60 | 61 | return self._rename_entity(action, sdc_id, params) 62 | 63 | def set_performance_profile(self, sdc_id, perf_profile): 64 | """Apply a performance profile to the specified SDC. 65 | 66 | :type sdc_id: str 67 | :type perf_profile: str 68 | :rtype: dict 69 | """ 70 | 71 | action = 'setSdcPerformanceParameters' 72 | 73 | params = {"perfProfile": perf_profile} 74 | return self._perform_entity_operation_based_on_action( 75 | sdc_id, action, params=params, add_entity=False) 76 | 77 | def query_selected_statistics(self, properties, ids=None): 78 | """Query PowerFlex SDC statistics. 79 | 80 | :type properties: list 81 | :type ids: list of SDC IDs or None for all SDC 82 | :rtype: dict 83 | """ 84 | 85 | action = "querySelectedStatistics" 86 | 87 | params = {'properties': properties} 88 | 89 | if ids: 90 | params["ids"] = ids 91 | else: 92 | params["allIds"] = "" 93 | 94 | return self._query_selected_statistics(action, params) 95 | -------------------------------------------------------------------------------- /PyPowerFlex/objects/sdt.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for interacting with SDT APIs.""" 17 | 18 | # pylint: disable=too-few-public-methods,no-member,too-many-arguments,too-many-positional-arguments 19 | 20 | import logging 21 | import requests 22 | from PyPowerFlex import base_client 23 | from PyPowerFlex import exceptions 24 | from PyPowerFlex import utils 25 | 26 | LOG = logging.getLogger(__name__) 27 | 28 | 29 | class SdtIp(dict): 30 | """PowerFlex sdt ip object. 31 | 32 | JSON-serializable, should be used as `sdt_ips` list item 33 | in `Sdt.create` method or sdt_ip item in `Sdt.add_sdt_ip` method. 34 | """ 35 | 36 | def __init__(self, ip, role): 37 | params = utils.prepare_params( 38 | { 39 | "ip": ip, 40 | "role": role, 41 | }, 42 | dump=False, 43 | ) 44 | super().__init__(**params) 45 | 46 | 47 | class SdtIpRoles: 48 | """SDT ip roles.""" 49 | 50 | storage_only = "StorageOnly" 51 | host_only = "HostOnly" 52 | storage_and_host = "StorageAndHost" 53 | 54 | 55 | class Sdt(base_client.EntityRequest): 56 | """ 57 | A class representing SDT client. 58 | """ 59 | def create( 60 | self, 61 | sdt_ips, 62 | sdt_name, 63 | protection_domain_id, 64 | storage_port=None, 65 | nvme_port=None, 66 | discovery_port=None, 67 | ): 68 | """Create PowerFlex SDT. 69 | 70 | :type sdt_ips: list[dict] 71 | :type storage_port: int 72 | :type nvme_port: int 73 | :type discovery_port: int 74 | :type sdt_name: str 75 | :type protection_domain_id: str 76 | :rtype: dict 77 | """ 78 | 79 | params = { 80 | "ips": sdt_ips, 81 | "storagePort": storage_port, 82 | "nvmePort": nvme_port, 83 | "discoveryPort": discovery_port, 84 | "name": sdt_name, 85 | "protectionDomainId": protection_domain_id, 86 | } 87 | 88 | return self._create_entity(params) 89 | 90 | def rename(self, sdt_id, name): 91 | """Rename PowerFlex SDT. 92 | 93 | :type sdt_id: str 94 | :type name: str 95 | :rtype: dict 96 | """ 97 | 98 | action = "renameSdt" 99 | 100 | params = {'newName': name} 101 | 102 | return self._rename_entity(action, sdt_id, params) 103 | 104 | def add_ip(self, sdt_id, ip, role): 105 | """Add PowerFlex SDT target IP address. 106 | 107 | :type sdt_id: str 108 | :type ip: str 109 | :type role: str 110 | :rtype: dict 111 | """ 112 | 113 | action = "addIp" 114 | 115 | params = { 116 | "ip": ip, 117 | "role": role, 118 | } 119 | 120 | r, response = self.send_post_request( 121 | self.base_action_url, 122 | action=action, 123 | entity=self.entity, 124 | entity_id=sdt_id, 125 | params=params, 126 | ) 127 | if r.status_code != requests.codes.ok: 128 | msg = ( 129 | f"Failed to add IP for PowerFlex {self.entity} " 130 | f"with id {sdt_id}. Error: {response}" 131 | ) 132 | LOG.error(msg) 133 | raise exceptions.PowerFlexClientException(msg) 134 | 135 | return self.get(entity_id=sdt_id) 136 | 137 | def remove_ip(self, sdt_id, ip): 138 | """Remove PowerFlex SDT target IP address. 139 | 140 | :type sdt_id: str 141 | :type ip: str 142 | :rtype: dict 143 | """ 144 | 145 | action = "removeIp" 146 | 147 | params = {"ip": ip} 148 | 149 | r, response = self.send_post_request( 150 | self.base_action_url, 151 | action=action, 152 | entity=self.entity, 153 | entity_id=sdt_id, 154 | params=params, 155 | ) 156 | if r.status_code != requests.codes.ok: 157 | msg = f"Failed to remove IP from PowerFlex {self.entity} " \ 158 | f"with id {sdt_id}. Error: {response}" 159 | LOG.error(msg) 160 | raise exceptions.PowerFlexClientException(msg) 161 | 162 | return self.get(entity_id=sdt_id) 163 | 164 | def set_ip_role(self, sdt_id, ip, role): 165 | """Set PowerFlex SDT target IP address role. 166 | 167 | :type sdt_id: str 168 | :type ip: str 169 | :param role: one of predefined attributes of SdtIpRoles 170 | :type role: str 171 | :rtype: dict 172 | """ 173 | 174 | action = "modifyIpRole" 175 | 176 | params = { 177 | "ip": ip, 178 | "newRole": role 179 | } 180 | 181 | r, response = self.send_post_request( 182 | self.base_action_url, 183 | action=action, 184 | entity=self.entity, 185 | entity_id=sdt_id, 186 | params=params, 187 | ) 188 | if r.status_code != requests.codes.ok: 189 | msg = f"Failed to set ip role for PowerFlex {self.entity} " \ 190 | f"with id {sdt_id}. Error: {response}" 191 | LOG.error(msg) 192 | raise exceptions.PowerFlexClientException(msg) 193 | 194 | return self.get(entity_id=sdt_id) 195 | 196 | def set_storage_port(self, sdt_id, storage_port): 197 | """Set PowerFlex SDT storage port. 198 | 199 | :type sdt_id: str 200 | :type storage_port: int 201 | :rtype: dict 202 | """ 203 | 204 | action = "modifyStoragePort" 205 | 206 | params = {"newStoragePort": storage_port} 207 | 208 | r, response = self.send_post_request( 209 | self.base_action_url, 210 | action=action, 211 | entity=self.entity, 212 | entity_id=sdt_id, 213 | params=params, 214 | ) 215 | if r.status_code != requests.codes.ok: 216 | msg = ( 217 | f"Failed to set storage port for PowerFlex {self.entity} " 218 | f"with id {sdt_id}. Error: {response}" 219 | ) 220 | LOG.error(msg) 221 | raise exceptions.PowerFlexClientException(msg) 222 | 223 | return self.get(entity_id=sdt_id) 224 | 225 | def set_nvme_port(self, sdt_id, nvme_port): 226 | """Set PowerFlex SDT NVMe port. 227 | 228 | :type sdt_id: str 229 | :type nvme_port: int 230 | :rtype: dict 231 | """ 232 | 233 | action = "modifyNvmePort" 234 | 235 | params = {"newNvmePort": nvme_port} 236 | 237 | r, response = self.send_post_request( 238 | self.base_action_url, 239 | action=action, 240 | entity=self.entity, 241 | entity_id=sdt_id, 242 | params=params, 243 | ) 244 | if r.status_code != requests.codes.ok: 245 | msg = ( 246 | f"Failed to set nvme port for PowerFlex {self.entity} " 247 | f"with id {sdt_id}. Error: {response}" 248 | ) 249 | LOG.error(msg) 250 | raise exceptions.PowerFlexClientException(msg) 251 | 252 | return self.get(entity_id=sdt_id) 253 | 254 | def set_discovery_port(self, sdt_id, discovery_port): 255 | """Set PowerFlex SDT discovery port. 256 | 257 | :type sdt_id: str 258 | :type discovery_port: int 259 | :rtype: dict 260 | """ 261 | 262 | action = "modifyDiscoveryPort" 263 | 264 | params = {"newDiscoveryPort": discovery_port} 265 | 266 | r, response = self.send_post_request( 267 | self.base_action_url, 268 | action=action, 269 | entity=self.entity, 270 | entity_id=sdt_id, 271 | params=params, 272 | ) 273 | if r.status_code != requests.codes.ok: 274 | msg = ( 275 | f"Failed to set discovery port for PowerFlex {self.entity} " 276 | f"with id {sdt_id}. Error: {response}" 277 | ) 278 | LOG.error(msg) 279 | raise exceptions.PowerFlexClientException(msg) 280 | 281 | return self.get(entity_id=sdt_id) 282 | 283 | def enter_maintenance_mode(self, sdt_id): 284 | """Enter Maintenance Mode. 285 | 286 | :type sdt_id: str 287 | :rtype: dict 288 | """ 289 | 290 | action = "enterMaintenanceMode" 291 | 292 | r, response = self.send_post_request( 293 | self.base_action_url, 294 | action=action, 295 | entity=self.entity, 296 | entity_id=sdt_id, 297 | params=None, 298 | ) 299 | if r.status_code != requests.codes.ok: 300 | msg = ( 301 | f"Failed to enter maintenance mode for PowerFlex {self.entity} " 302 | f"with id {sdt_id}. Error: {response}" 303 | ) 304 | LOG.error(msg) 305 | raise exceptions.PowerFlexClientException(msg) 306 | 307 | return self.get(entity_id=sdt_id) 308 | 309 | def exit_maintenance_mode(self, sdt_id): 310 | """Exit Maintenance Mode. 311 | 312 | :type sdt_id: str 313 | :rtype: dict 314 | """ 315 | 316 | action = "exitMaintenanceMode" 317 | 318 | r, response = self.send_post_request( 319 | self.base_action_url, 320 | action=action, 321 | entity=self.entity, 322 | entity_id=sdt_id, 323 | params=None, 324 | ) 325 | if r.status_code != requests.codes.ok: 326 | msg = ( 327 | f"Failed to exit maintenance mode for PowerFlex {self.entity} " 328 | f"with id {sdt_id}. Error: {response}" 329 | ) 330 | LOG.error(msg) 331 | raise exceptions.PowerFlexClientException(msg) 332 | 333 | return self.get(entity_id=sdt_id) 334 | 335 | def delete(self, sdt_id, force=None): 336 | """Remove PowerFlex SDT. 337 | 338 | :type sdt_id: str 339 | :type force: bool 340 | :rtype: None 341 | """ 342 | 343 | params = {"force": force} 344 | 345 | return self._delete_entity(sdt_id, params) 346 | -------------------------------------------------------------------------------- /PyPowerFlex/objects/service_template.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for interacting with service template APIs.""" 17 | 18 | # pylint: disable=arguments-renamed,no-member,too-many-arguments,too-many-positional-arguments 19 | 20 | import logging 21 | import requests 22 | from PyPowerFlex import base_client 23 | from PyPowerFlex import exceptions 24 | from PyPowerFlex import utils 25 | LOG = logging.getLogger(__name__) 26 | 27 | 28 | class ServiceTemplate(base_client.EntityRequest): 29 | """ 30 | A class representing Service Template client. 31 | """ 32 | def get( 33 | self, 34 | filters=None, 35 | full=None, 36 | limit=None, 37 | offset=None, 38 | sort=None, 39 | include_attachments=None): 40 | """ 41 | Retrieve all Service Templates with filter, sort, pagination 42 | :param filters: (Optional) The filters to apply to the results. 43 | :param full: (Optional) Whether to return full details for each result. 44 | :param limit: (Optional) Page limit. 45 | :param offset: (Optional) Pagination offset. 46 | :param sort: (Optional) The field to sort the results by. 47 | :param include_attachments: (Optional) Whether to include attachments. 48 | :return: A list of dictionary containing the retrieved Service Templates. 49 | """ 50 | params = { 51 | "filter": filters, 52 | "full": full, 53 | "limit": limit, 54 | "offset": offset, 55 | "sort": sort, 56 | "includeAttachments": include_attachments 57 | } 58 | r, response = self.send_get_request( 59 | utils.build_uri_with_params( 60 | self.service_template_url, **params)) 61 | if r.status_code != requests.codes.ok: 62 | msg = f'Failed to retrieve service templates. Error: {response}' 63 | LOG.error(msg) 64 | raise exceptions.PowerFlexClientException(msg) 65 | return response 66 | 67 | def get_by_id(self, service_template_id, for_deployment=False): 68 | """ 69 | Retrieve a Service Template by its ID. 70 | :param service_template_id: The ID of the Service Template to retrieve. 71 | :param for_deployment: (Optional) Whether to retrieve the Service Template for deployment. 72 | :return: A dictionary containing the retrieved Service Template. 73 | """ 74 | url = f'{self.service_template_url}/{service_template_id}' 75 | if for_deployment: 76 | url += '?forDeployment=true' 77 | r, response = self.send_get_request(url) 78 | if r.status_code != requests.codes.ok: 79 | msg = ( 80 | f'Failed to retrieve service template by id {service_template_id}. ' 81 | f'Error: {response}' 82 | ) 83 | LOG.error(msg) 84 | raise exceptions.PowerFlexClientException(msg) 85 | return response 86 | -------------------------------------------------------------------------------- /PyPowerFlex/objects/snapshot_policy.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for interacting with snapshot policy APIs.""" 17 | 18 | # pylint: disable=too-few-public-methods,no-member,too-many-arguments,too-many-positional-arguments,duplicate-code 19 | 20 | import logging 21 | 22 | import requests 23 | 24 | from PyPowerFlex import base_client 25 | from PyPowerFlex import exceptions 26 | 27 | 28 | LOG = logging.getLogger(__name__) 29 | 30 | 31 | class AutoSnapshotRemovalAction: 32 | """Auto snapshot deletion strategy.""" 33 | 34 | detach = 'Detach' 35 | remove = 'Remove' 36 | 37 | 38 | class SnapshotPolicy(base_client.EntityRequest): 39 | """ 40 | A class representing Snapshot Policy client. 41 | """ 42 | def add_source_volume(self, snapshot_policy_id, volume_id): 43 | """Add source volume to PowerFlex snapshot policy. 44 | 45 | :type snapshot_policy_id: str 46 | :type volume_id: str 47 | :rtype: dict 48 | """ 49 | 50 | action = 'addSourceVolumeToSnapshotPolicy' 51 | 52 | params = {"sourceVolumeId": volume_id} 53 | 54 | r, response = self.send_post_request(self.base_action_url, 55 | action=action, 56 | entity=self.entity, 57 | entity_id=snapshot_policy_id, 58 | params=params) 59 | if r.status_code != requests.codes.ok: 60 | msg = ( 61 | f"Failed to add source volume to PowerFlex {self.entity} " 62 | f"with id {snapshot_policy_id}. " 63 | f"Error: {response}" 64 | ) 65 | LOG.error(msg) 66 | raise exceptions.PowerFlexClientException(msg) 67 | 68 | return self.get(entity_id=snapshot_policy_id) 69 | 70 | def create(self, 71 | auto_snap_creation_cadence_in_min, 72 | retained_snaps_per_level, 73 | name=None, 74 | paused=None, 75 | snapshot_access_mode=None, 76 | secure_snapshots=None): 77 | """Create PowerFlex snapshot policy. 78 | 79 | :type auto_snap_creation_cadence_in_min: int 80 | :type retained_snaps_per_level: list[int] 81 | :type name: str 82 | :type paused: bool 83 | :rtype: dict 84 | """ 85 | 86 | params = { 87 | "autoSnapshotCreationCadenceInMin": auto_snap_creation_cadence_in_min, 88 | "numOfRetainedSnapshotsPerLevel": retained_snaps_per_level, 89 | "name": name, 90 | "paused": paused, 91 | "snapshotAccessMode": snapshot_access_mode, 92 | "secureSnapshots": secure_snapshots 93 | } 94 | 95 | return self._create_entity(params) 96 | 97 | def delete(self, snapshot_policy_id): 98 | """Remove PowerFlex snapshot policy. 99 | 100 | :type snapshot_policy_id: str 101 | :rtype: None 102 | """ 103 | 104 | return self._delete_entity(snapshot_policy_id) 105 | 106 | def modify(self, 107 | snapshot_policy_id, 108 | auto_snap_creation_cadence_in_min, 109 | retained_snaps_per_level): 110 | """Modify PowerFlex snapshot policy. 111 | 112 | :type snapshot_policy_id: str 113 | :type auto_snap_creation_cadence_in_min: int 114 | :type retained_snaps_per_level: list[int] 115 | :rtype: dict 116 | """ 117 | 118 | action = 'modifySnapshotPolicy' 119 | 120 | params = { 121 | "autoSnapshotCreationCadenceInMin": auto_snap_creation_cadence_in_min, 122 | "numOfRetainedSnapshotsPerLevel": retained_snaps_per_level 123 | } 124 | 125 | r, response = self.send_post_request(self.base_action_url, 126 | action=action, 127 | entity=self.entity, 128 | entity_id=snapshot_policy_id, 129 | params=params) 130 | if r.status_code != requests.codes.ok: 131 | msg = ( 132 | f"Failed to modify PowerFlex {self.entity} with id {snapshot_policy_id}. " 133 | f"Error: {response}" 134 | ) 135 | LOG.error(msg) 136 | raise exceptions.PowerFlexClientException(msg) 137 | 138 | return self.get(entity_id=snapshot_policy_id) 139 | 140 | def pause(self, snapshot_policy_id): 141 | """Pause PowerFlex snapshot policy. 142 | 143 | :type snapshot_policy_id: str 144 | :rtype: dict 145 | """ 146 | 147 | action = 'pauseSnapshotPolicy' 148 | 149 | r, response = self.send_post_request(self.base_action_url, 150 | action=action, 151 | entity=self.entity, 152 | entity_id=snapshot_policy_id) 153 | if r.status_code != requests.codes.ok: 154 | msg = ( 155 | f"Failed to pause PowerFlex {self.entity} with id {snapshot_policy_id}. " 156 | f"Error: {response}" 157 | ) 158 | LOG.error(msg) 159 | raise exceptions.PowerFlexClientException(msg) 160 | 161 | return self.get(entity_id=snapshot_policy_id) 162 | 163 | def remove_source_volume(self, 164 | snapshot_policy_id, 165 | volume_id, 166 | auto_snap_removal_action, 167 | detach_locked_auto_snaps=None): 168 | """Remove source volume from PowerFlex snapshot policy. 169 | 170 | :type snapshot_policy_id: str 171 | :type volume_id: str 172 | :param auto_snap_removal_action: one of predefined attributes of 173 | AutoSnapshotRemovalAction 174 | :type auto_snap_removal_action: str 175 | :type detach_locked_auto_snaps: bool 176 | :rtype: dict 177 | """ 178 | 179 | action = 'removeSourceVolumeFromSnapshotPolicy' 180 | 181 | params = { 182 | "sourceVolumeId": volume_id, 183 | "autoSnapshotRemovalAction": auto_snap_removal_action, 184 | "detachLockedAutoSnapshots": detach_locked_auto_snaps 185 | } 186 | 187 | r, response = self.send_post_request(self.base_action_url, 188 | action=action, 189 | entity=self.entity, 190 | entity_id=snapshot_policy_id, 191 | params=params) 192 | if r.status_code != requests.codes.ok: 193 | msg = ( 194 | f"Failed to remove source volume from PowerFlex {self.entity} " 195 | f"with id {snapshot_policy_id}. " 196 | f"Error: {response}" 197 | ) 198 | LOG.error(msg) 199 | raise exceptions.PowerFlexClientException(msg) 200 | 201 | return self.get(entity_id=snapshot_policy_id) 202 | 203 | def rename(self, snapshot_policy_id, name): 204 | """Rename PowerFlex snapshot policy. 205 | 206 | :type snapshot_policy_id: str 207 | :type name: str 208 | :rtype: dict 209 | """ 210 | 211 | action = 'renameSnapshotPolicy' 212 | 213 | params = { 214 | "newName": name 215 | } 216 | 217 | return self._rename_entity(action, snapshot_policy_id, params) 218 | 219 | def resume(self, snapshot_policy_id): 220 | """Resume PowerFlex snapshot policy. 221 | 222 | :type snapshot_policy_id: str 223 | :rtype: dict 224 | """ 225 | 226 | action = 'resumeSnapshotPolicy' 227 | 228 | r, response = self.send_post_request(self.base_action_url, 229 | action=action, 230 | entity=self.entity, 231 | entity_id=snapshot_policy_id) 232 | if r.status_code != requests.codes.ok: 233 | msg = ( 234 | f"Failed to resume PowerFlex {self.entity} with id {snapshot_policy_id}. " 235 | f"Error: {response}" 236 | ) 237 | LOG.error(msg) 238 | raise exceptions.PowerFlexClientException(msg) 239 | 240 | return self.get(entity_id=snapshot_policy_id) 241 | 242 | def get_statistics(self, snapshot_policy_id, fields=None): 243 | """Get related PowerFlex Statistics for snapshot policy. 244 | 245 | :type snapshot_policy_id: str 246 | :type fields: list|tuple 247 | :rtype: dict 248 | """ 249 | 250 | return self.get_related(snapshot_policy_id, 251 | 'Statistics', 252 | fields) 253 | 254 | def query_selected_statistics(self, properties, ids=None): 255 | """Query PowerFlex snapshot policy statistics. 256 | 257 | :type properties: list 258 | :type ids: list of snapshot policy IDs or None for all snapshot 259 | policies 260 | :rtype: dict 261 | """ 262 | 263 | action = "querySelectedStatistics" 264 | 265 | params = {'properties': properties} 266 | 267 | if ids: 268 | params["ids"] = ids 269 | else: 270 | params["allIds"] = "" 271 | 272 | return self._query_selected_statistics(action, params) 273 | -------------------------------------------------------------------------------- /PyPowerFlex/objects/utility.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Utility module for PowerFlex.""" 17 | 18 | # pylint: disable=no-member,useless-parent-delegation 19 | import logging 20 | 21 | import requests 22 | 23 | from PyPowerFlex import base_client 24 | from PyPowerFlex import exceptions 25 | from PyPowerFlex.constants import StoragePoolConstants, VolumeConstants, SnapshotPolicyConstants 26 | 27 | 28 | LOG = logging.getLogger(__name__) 29 | 30 | 31 | class PowerFlexUtility(base_client.EntityRequest): 32 | "Utility class for PowerFlex" 33 | def __init__(self, token, configuration): 34 | super().__init__(token, configuration) 35 | 36 | def get_statistics_for_all_storagepools(self, ids=None, properties=None): 37 | """list storagepool statistics for PowerFlex. 38 | 39 | :param ids: list 40 | :param properties: list 41 | :return: dict 42 | """ 43 | 44 | action = 'querySelectedStatistics' 45 | version = self.get_api_version() 46 | default_properties = StoragePoolConstants.DEFAULT_STATISTICS_PROPERTIES 47 | if version != '3.5': 48 | default_properties = default_properties + \ 49 | StoragePoolConstants.DEFAULT_STATISTICS_PROPERTIES_ABOVE_3_5 50 | params = { 51 | 'properties': default_properties if properties is None else properties} 52 | if ids is None: 53 | params['allIds'] = "" 54 | else: 55 | params['ids'] = ids 56 | 57 | r, response = self.send_post_request(self.list_statistics_url, 58 | entity='StoragePool', 59 | action=action, 60 | params=params) 61 | if r.status_code != requests.codes.ok: 62 | msg = ( 63 | f"Failed to list storage pool statistics for PowerFlex. " 64 | f"Error: {response}" 65 | ) 66 | LOG.error(msg) 67 | raise exceptions.PowerFlexClientException(msg) 68 | 69 | return response 70 | 71 | def get_statistics_for_all_volumes(self, ids=None, properties=None): 72 | """list volume statistics for PowerFlex. 73 | 74 | :param ids: list 75 | :param properties: list 76 | :return: dict 77 | """ 78 | 79 | action = 'querySelectedStatistics' 80 | 81 | params = { 82 | 'properties': ( 83 | VolumeConstants.DEFAULT_STATISTICS_PROPERTIES 84 | if properties is None 85 | else properties 86 | ) 87 | } 88 | if ids is None: 89 | params['allIds'] = "" 90 | else: 91 | params['ids'] = ids 92 | 93 | r, response = self.send_post_request(self.list_statistics_url, 94 | entity='Volume', 95 | action=action, 96 | params=params) 97 | if r.status_code != requests.codes.ok: 98 | msg = ( 99 | 'Failed to list volume statistics for PowerFlex. ' 100 | f'Error: {response}' 101 | ) 102 | LOG.error(msg) 103 | raise exceptions.PowerFlexClientException(msg) 104 | 105 | return response 106 | 107 | def get_statistics_for_all_snapshot_policies( 108 | self, ids=None, properties=None): 109 | """list snapshot policy statistics for PowerFlex. 110 | 111 | :param ids: list 112 | :param properties: list 113 | :return: dict 114 | """ 115 | 116 | action = 'querySelectedStatistics' 117 | 118 | params = {} 119 | if properties is None: 120 | params['properties'] = SnapshotPolicyConstants.DEFAULT_STATISTICS_PROPERTIES 121 | else: 122 | params['properties'] = properties 123 | if ids is None: 124 | params['allIds'] = "" 125 | else: 126 | params['ids'] = ids 127 | 128 | r, response = self.send_post_request(self.list_statistics_url, 129 | entity='SnapshotPolicy', 130 | action=action, 131 | params=params) 132 | if r.status_code != requests.codes.ok: 133 | msg = ( 134 | f"Failed to list snapshot policy statistics for PowerFlex. " 135 | f"Error: {response}" 136 | ) 137 | LOG.error(msg) 138 | raise exceptions.PowerFlexClientException(msg) 139 | 140 | return response 141 | -------------------------------------------------------------------------------- /PyPowerFlex/token.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """This module is used for the management of token.""" 17 | 18 | class Token: 19 | """ 20 | A class to manage a token. 21 | """ 22 | def __init__(self): 23 | """ 24 | Initialize the Token object. 25 | 26 | The initial value of the token is None. 27 | """ 28 | self.__token = None 29 | 30 | def get(self): 31 | """ 32 | Get the current token. 33 | 34 | Returns: 35 | The current token. 36 | """ 37 | return self.__token 38 | 39 | def set(self, token): 40 | """ 41 | Set the token. 42 | 43 | Args: 44 | token (Any): The new token. 45 | 46 | Returns: 47 | None 48 | """ 49 | self.__token = token 50 | -------------------------------------------------------------------------------- /PyPowerFlex/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """This module contains the utils used in the code.""" 17 | 18 | import json 19 | import logging 20 | import numbers 21 | import sys 22 | 23 | from PyPowerFlex import exceptions 24 | 25 | def init_logger(log_level): 26 | """Initialize logger for PowerFlex client. 27 | 28 | :param log_level: logging level (e. g. logging.DEBUG) 29 | :type log_level: int 30 | :rtype: None 31 | """ 32 | 33 | logging.basicConfig( 34 | stream=sys.stdout, 35 | level=log_level or logging.ERROR, 36 | format='[%(levelname)s] %(asctime)s ' 37 | '%(name)s:%(funcName)s:%(lineno)s: %(message)s' 38 | ) 39 | 40 | 41 | def filter_response(response, filter_fields): 42 | """Filter PowerFlex API response by fields provided in `filter_fields`. 43 | 44 | Supports only flat filtering. Case-sensitive. 45 | 46 | :param response: PowerFlex API response 47 | :param filter_fields: key-value pairs of filter field and its value 48 | :type filter_fields: dict 49 | :return: filtered response 50 | :rtype: list 51 | """ 52 | 53 | def filter_func(obj): 54 | for filter_key, filter_value in filter_fields.items(): 55 | try: 56 | response_value = obj[filter_key] 57 | response_value, filter_value = map( 58 | lambda value: [value] 59 | if not isinstance(value, (list, tuple)) 60 | else value, 61 | [response_value, filter_value]) 62 | if not set(response_value).intersection(filter_value): 63 | return False 64 | except (KeyError, TypeError): 65 | return False 66 | return True 67 | 68 | return list(filter(filter_func, response)) 69 | 70 | 71 | def query_response_fields(response, fields): 72 | """Extract specified fields from PowerFlex API response. 73 | 74 | :param response: PowerFlex API response 75 | :param fields: fields to extract 76 | :type fields: list | tuple 77 | :return: response containing only specified fields 78 | :rtype: list | dict 79 | """ 80 | 81 | def query_entity_fields(entity): 82 | entity_fields = {} 83 | fields_not_found = [] 84 | for field in fields: 85 | try: 86 | entity_fields[field] = entity[field] 87 | except (KeyError, TypeError): 88 | fields_not_found.append(field) 89 | if fields_not_found: 90 | msg = ( 91 | 'The following fields are not found in response: ' 92 | f'{", ".join(fields_not_found)}.' 93 | ) 94 | raise exceptions.FieldsNotFound(msg) 95 | return entity_fields 96 | 97 | if isinstance(response, list): 98 | return list(map(query_entity_fields, response)) 99 | if isinstance(response, dict): 100 | return query_entity_fields(response) 101 | return None 102 | 103 | 104 | def convert(param): 105 | """ 106 | Convert parameters to appropriate types. 107 | 108 | :param param: request parameters 109 | :return: converted parameters 110 | """ 111 | if isinstance(param, list): 112 | return [convert(item) for item in param] 113 | if isinstance(param, (numbers.Number, bool)): 114 | # Convert numbers and boolean to string. 115 | return str(param) 116 | # Other types are not converted. 117 | return param 118 | 119 | 120 | def prepare_params(params, dump=True): 121 | """Prepare request parameters before sending. 122 | 123 | :param params: request parameters 124 | :type params: dict 125 | :param dump: dump params to json 126 | :return: prepared parameters 127 | """ 128 | 129 | if not isinstance(params, dict): 130 | return params 131 | 132 | prepared = {} 133 | for name, value in params.items(): 134 | if value is not None: 135 | prepared[name] = convert(value) 136 | if dump: 137 | return json.dumps(prepared) 138 | return prepared 139 | 140 | 141 | def is_version_3(version): 142 | """ Check the API version. 143 | 144 | :param version: Specifies the current API version 145 | :return: True if API version is lesser than 4.0 146 | :rtype: bool 147 | """ 148 | appliance_version = "4.0" 149 | if version < appliance_version: 150 | return True 151 | return False 152 | 153 | 154 | def build_uri_with_params(uri, **url_params): 155 | """ 156 | Build the URI with query parameters. 157 | 158 | :param uri: base URI 159 | :param url_params: query parameters 160 | :return: URI with query parameters 161 | """ 162 | query_params = [ 163 | f"{key}={item}" if isinstance( 164 | value, 165 | list) else f"{key}={value}" for key, 166 | value in url_params.items() for item in ( 167 | value if isinstance( 168 | value, 169 | list) else [value]) if item is not None] 170 | if query_params: 171 | uri += '?' + '&'.join(query_params) 172 | return uri 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyPowerFlex 2 | 3 | Python SDK for Dell PowerFlex. 4 | 5 | Supports PowerFlex (VxFlex OS) version 3.0 and later. 6 | 7 | ## Installation and usage 8 | 9 | ### Installation 10 | 11 | ```shell script 12 | python setup.py install 13 | ``` 14 | 15 | ### Usage 16 | 17 | #### Configuration options 18 | 19 | | Option | Description | 20 | | :---: | :---: | 21 | | gateway_address | (str) PowerFlex API address. | 22 | | gateway_port | (int) PowerFlex API port. **Default**: 443. | 23 | | username | (str) PowerFlex API username. | 24 | | password | (str) PowerFlex API password. | 25 | | verify_certificate | (bool) Verify server's certificate. **Default**: False. | 26 | | certificate_path | (str) Path to server's certificate. **Default**: None. | 27 | | timeout | (int) Timeout for PowerFlex API request **Default**: 120. 28 | | log_level | (int) Logging level (e. g. logging.DEBUG). **Default**: logging.ERROR. | 29 | 30 | #### Available resources 31 | 32 | * Device 33 | * ProtectionDomain 34 | * Sds 35 | * Sdt 36 | * SnapshotPolicy 37 | * ReplicationConsistencyGroup 38 | * ReplicationPair 39 | * System 40 | * StoragePool 41 | * AccelerationPool 42 | * Sdc 43 | * NvmeHost 44 | * FaultSet 45 | * Volume 46 | * ManagedDevice 47 | * Deployment 48 | * ServiceTemplate 49 | * FirmwareRepository 50 | 51 | #### Initialize PowerFlex client 52 | 53 | ```python 54 | from PyPowerFlex import PowerFlexClient 55 | 56 | client = PowerFlexClient(gateway_address='1.2.3.4', 57 | gateway_port=443, 58 | username='admin', 59 | password='admin') 60 | client.initialize() 61 | ``` 62 | 63 | #### Filtering and fields querying 64 | 65 | SDK supports flat filtering and fields querying. 66 | 67 | ```python 68 | from PyPowerFlex.objects.device import MediaType 69 | 70 | client.device.get(filter_fields={'mediaType': MediaType.ssd, 71 | 'name': ['/dev/sdd', '/dev/sde']}, 72 | fields=['id', 'name', 'mediaType']) 73 | 74 | [{'id': '3eddd9dd00010003', 'mediaType': 'SSD', 'name': '/dev/sde'}, 75 | {'id': '3edcd9d800000002', 'mediaType': 'SSD', 'name': '/dev/sdd'}, 76 | {'id': '3edcd9d900000003', 'mediaType': 'SSD', 'name': '/dev/sde'}, 77 | {'id': '3edd00e900010002', 'mediaType': 'SSD', 'name': '/dev/sdd'}, 78 | {'id': '3eded9e000020002', 'mediaType': 'SSD', 'name': '/dev/sdd'}, 79 | {'id': '3eded9e100020003', 'mediaType': 'SSD', 'name': '/dev/sde'}] 80 | ``` 81 | 82 | #### Examples 83 | 84 | ```python 85 | # Create device 86 | from PyPowerFlex.objects.device import MediaType 87 | client.device.create('/dev/sdd', 88 | sds_id='63471cdd00000001', 89 | media_type=MediaType.ssd, 90 | storage_pool_id='889dd7b900000000', 91 | name='/dev/sdd') 92 | 93 | # Rename device 94 | client.device.get(filter_fields={'name': '/dev/sdd', 'id': '3eddd9dc00010002'}, 95 | fields=['name', 'id']) 96 | [{'name': '/dev/sdd', 'id': '3eddd9dc00010002'}] 97 | 98 | client.device.rename('3eddd9dc00010002', '/dev/sdd-renamed') 99 | client.device.get(filter_fields={'id': '3eddd9dc00010002'}, 100 | fields=['name', 'id']) 101 | [{'name': '/dev/sdd-renamed', 'id': '3eddd9dc00010002'}] 102 | 103 | # Remove device 104 | client.device.delete('3eddd9dc00010002') 105 | {} 106 | 107 | # Get Protection Domain related SDSs 108 | client.protection_domain.get_sdss('b922fb3700000000', fields=['ipList', 'name']) 109 | [{'ipList': [{'ip': '192.100.xxx.xxx', 'role': 'all'}, 110 | {'ip': '172.160.xxx.xxx', 'role': 'sdcOnly'}]}, 111 | {'ipList': [{'ip': '192.101.xxx.xxx', 'role': 'all'}, 112 | {'ip': '172.161.xxx.xxx', 'role': 'sdcOnly'}]}, 113 | {'ipList': [{'ip': '192.102.xxx.xxx', 'role': 'all'}, 114 | {'ip': '172.162.xxx.xxx', 'role': 'sdcOnly'}]}] 115 | 116 | # Delete protection domain 117 | client.protection_domain.delete('9300c1f900000000') 118 | {} 119 | 120 | # Add SDS IP-address 121 | from PyPowerFlex.objects.sds import SdsIp 122 | from PyPowerFlex.objects.sds import SdsIpRoles 123 | 124 | client.sds.add_ip('63471cdc00000000', SdsIp('172.17.xxx.xxx', SdsIpRoles.sdc_only)) 125 | client.sds.get(filter_fields={'id': '63471cdc00000000'}, fields=['id', 'ipList']) 126 | [{'id': '63471cdc00000000', 127 | 'ipList': [{'ip': '192.168.xxx.xxx', 'role': 'all'}, 128 | {'ip': '172.16.xxx.xxx', 'role': 'sdcOnly'}, 129 | {'ip': '172.17.xxx.xxx', 'role': 'sdcOnly'}]}] 130 | 131 | # Set SDS IP-address role 132 | client.sds.set_ip_role('63471cdc00000000', '172.17.xxx.xxx', SdsIpRoles.sds_only, force=True) 133 | [{'id': '63471cdc00000000', 134 | 'ipList': [{'ip': '192.168.xxx.xxx', 'role': 'all'}, 135 | {'ip': '172.16.xxx.xxx', 'role': 'sdcOnly'}, 136 | {'ip': '172.17.xxx.xxx', 'role': 'sdsOnly'}]}] 137 | 138 | # Remove SDS IP-address 139 | client.sds.remove_ip('63471cdc00000000', '172.16.xxx.xxx') 140 | [{'id': '63471cdc00000000', 141 | 'ipList': [{'ip': '192.168.xxx.xxx', 'role': 'all'}, 142 | {'ip': '172.17.xxx.xxx', 'role': 'sdcOnly'}]}] 143 | 144 | # Create snapshot policy 145 | client.snapshot_policy.create(15, [3, 4, 5, 6]) 146 | 147 | # Rename snapshot policy 148 | client.snapshot_policy.rename('f047913500000000', 'SnapshotPolicy_sp2') 149 | 150 | # Snapshot volumes 151 | from PyPowerFlex.objects.system import SnapshotDef 152 | 153 | system_id = client.system.get(fields=['id'])[0]['id'] 154 | client.system.snapshot_volumes(system_id, 155 | [SnapshotDef('afa52f0c00000003', 'snap1'), 156 | SnapshotDef('afa52f0c00000003', 'snap2')]) 157 | {'snapshotGroupId': '5aaf81e800000002', 'volumeIdList': ['afa5561900000007', 'afa5561a00000008']} 158 | 159 | # Remove ConsistencyGroup snapshot 160 | client.system.remove_cg_snapshots(system_id, '5aaf81e800000002') 161 | {'numberOfVolumes': 2} 162 | 163 | # Rename storage pool 164 | client.storage_pool.rename('dbd4dbcd00000000', 'StoragePool_sp2') 165 | client.storage_pool.get(filter_fields={'id': 'dbd4dbcd00000000'}, 166 | fields=['name', 'id']) 167 | [{'name': 'StoragePool_sp2', 'id': 'dbd4dbcd00000000'}] 168 | 169 | # Set media tye for storage pool 170 | from PyPowerFlex.objects.storage_pool import MediaType 171 | client.storage_pool.set_media_type(storage_pool_id='dbd4dbcd00000000', 172 | media_type=MediaType.ssd, 173 | override_device_configuration=None) 174 | 175 | # Create acceleration pool 176 | from PyPowerFlex.objects.acceleration_pool import MediaType 177 | client.acceleration_pool.create(media_type=MediaType.ssd, 178 | protection_domain_id='1caf743100000000', 179 | name='ACP_SSD', 180 | is_rfcache=True) 181 | client.acceleration_pool.get(filter_fields={'id': '9c8c5c7800000001'}, 182 | fields=['name', 'id']) 183 | [{'name': 'ACP_SSD', 'id': '9c8c5c7800000001'}] 184 | 185 | # Delete acceleration pool 186 | client.acceleration_pool.delete('9c8c5c7800000001') 187 | {} 188 | 189 | # Rename SDC 190 | client.sdc.rename('a7e798d100000000', 'SDC_2') 191 | client.sdc.get(filter_fields={'id': 'a7e798d100000000'}, 192 | fields=['name', 'id']) 193 | [{'name': 'SDC_2', 'id': 'a7e798d100000000'}] 194 | 195 | # Set performance profile for SDC 196 | client.sdc.set_performance_profile('a7e798d100000000', 'HighPerformance') 197 | 198 | # Add a fault set to a protection domain 199 | client.fault_set.create(protection_domain_id='dc65bd9900000000', 200 | name='sio-fs1') 201 | client.fault_set.get(filter_fields={'id': 'fba27fae00000001'}, 202 | fields=['name', 'id']) 203 | [{'name': 'sio-fs1', 'id': 'fba27fae00000001'}] 204 | 205 | # Clear fault set 206 | client.fault_set.clear('fba27fae00000001') 207 | {} 208 | 209 | # Create volume 210 | from PyPowerFlex.objects.volume import VolumeType 211 | from PyPowerFlex.objects.volume import CompressionMethod 212 | client.volume.create(storage_pool_id='76f2b2fd00000000', 213 | size_in_gb=40, 214 | name='new_thin_vol', 215 | volume_type=VolumeType.thin, 216 | use_rmcache=True, 217 | compression_method=CompressionMethod.normal) 218 | 219 | # Extend volume size 220 | client.volume.extend(volume_id='4a3a153e00000000', 221 | size_in_gb=48) 222 | client.volume.get(filter_fields={'id': '4a3a153e00000000'}, 223 | fields=['name', 'id']) 224 | [{'name': 'sio-new_thin_vol', 'id': '4a3a153e00000000'}] 225 | ``` 226 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | # nonk8s 2 | apiVersion: backstage.io/v1alpha1 3 | kind: Component 4 | metadata: 5 | name: python-powerflex 6 | description: "Python SDK for Dell PowerFlex" 7 | tags: 8 | - ansible 9 | - devops 10 | - automation 11 | - storage 12 | - dell 13 | - infrastructure-as-code 14 | - powerflex 15 | annotations: 16 | backstage.io/techdocs-ref: dir:. 17 | github.com/project-slug: dell/python-powerflex 18 | links: 19 | - url: 'https://pypi.org/project/PyPowerFlex/' 20 | title: 'PyPI' 21 | icon: 'web' 22 | - url: 'https://github.com/dell/python-powerflex/issues' 23 | title: 'Contact Technical Support' 24 | icon: 'help' 25 | spec: 26 | type: service 27 | lifecycle: production 28 | owner: user:default/sachin_apagundi 29 | visibility: all 30 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | packaging 3 | 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for general setup.""" 17 | 18 | # pylint: disable=import-error 19 | 20 | from setuptools import setup 21 | 22 | setup( 23 | name='PyPowerFlex', 24 | version='1.14.0', 25 | description='Python library for Dell PowerFlex', 26 | author='Ansible Team at Dell', 27 | author_email='ansible.team@dell.com', 28 | install_requires=[ 29 | 'packaging>=20.4', 30 | 'requests>=2.23.0', 31 | ], 32 | license_files = ('LICENSE',), 33 | classifiers=['License :: OSI Approved :: Apache Software License'], 34 | packages=[ 35 | 'PyPowerFlex', 36 | 'PyPowerFlex.objects', 37 | ], 38 | python_requires='>=3.5' 39 | ) 40 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | bandit==1.6.2 2 | coverage==5.1 3 | flake8==3.7.9 4 | flake8-copyright==0.2.2 5 | flake8-import-order==0.18.1 6 | stestr==2.6.0 -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """This module is used for the initialization of the test framework.""" 17 | 18 | # pylint: disable=too-many-instance-attributes,keyword-arg-before-vararg,broad-exception-raised,unused-argument 19 | 20 | import collections 21 | import contextlib 22 | import json 23 | import logging 24 | from unittest import mock 25 | from unittest import TestCase 26 | 27 | import requests 28 | 29 | import PyPowerFlex 30 | from PyPowerFlex import utils 31 | 32 | 33 | class MockResponse(requests.Response): 34 | """ 35 | Mock HTTP Response. 36 | 37 | Defines http replies from mocked calls to do_request(). 38 | """ 39 | def __init__(self, content, status_code=200): 40 | """ 41 | Initialize a MockResponse. 42 | 43 | Args: 44 | content (str or dict): The content of the response. 45 | status_code (int): The status code of the response. 46 | """ 47 | super().__init__() 48 | self._content = content 49 | self.request = mock.MagicMock() 50 | self.status_code = status_code 51 | 52 | def json(self, **kwargs): 53 | """ 54 | Return the content of the response as JSON. 55 | 56 | Args: 57 | **kwargs: Additional keyword arguments. 58 | 59 | Returns: 60 | dict: The content of the response. 61 | """ 62 | return self._content 63 | 64 | @property 65 | def text(self): 66 | """ 67 | Return the content of the response as text. 68 | 69 | Returns: 70 | str: The content of the response. 71 | """ 72 | if not isinstance(self._content, bytes): 73 | return json.dumps(self._content) 74 | return super().text 75 | 76 | 77 | class PyPowerFlexTestCase(TestCase): 78 | """ 79 | Base test case for PyPowerFlex. 80 | 81 | Provides a mocked HTTP response for testing. 82 | """ 83 | RESPONSE_MODE = ( 84 | collections.namedtuple('RESPONSE_MODE', 'Valid Invalid BadStatus') 85 | (Valid='Valid', Invalid='Invalid', BadStatus='BadStatus') 86 | ) 87 | BAD_STATUS_RESPONSE = MockResponse( 88 | { 89 | 'errorCode': 500, 90 | 'message': 'Test default bad status', 91 | }, 500 92 | ) 93 | MOCK_RESPONSES = {} 94 | DEFAULT_MOCK_RESPONSES = { 95 | RESPONSE_MODE.Valid: { 96 | '/login': 'token', 97 | '/version': '3.5', 98 | '/logout': '', 99 | }, 100 | RESPONSE_MODE.Invalid: { 101 | '/version': '2.5', 102 | }, 103 | RESPONSE_MODE.BadStatus: { 104 | '/login': MockResponse( 105 | { 106 | 'errorCode': 1, 107 | 'message': 'Test login bad status', 108 | }, 400 109 | ), 110 | '/version': MockResponse( 111 | { 112 | 'errorCode': 2, 113 | 'message': 'Test version bad status', 114 | }, 400 115 | ), 116 | '/logout': MockResponse( 117 | { 118 | 'errorCode': 3, 119 | 'message': 'Test logout bad status', 120 | }, 400 121 | ) 122 | } 123 | } 124 | __http_response_mode = RESPONSE_MODE.Valid 125 | 126 | def setUp(self): 127 | """ 128 | Set up the test case. 129 | 130 | Creates a PyPowerFlex client and sets up mocked HTTP responses. 131 | """ 132 | self.gateway_address = '1.2.3.4' 133 | self.gateway_port = 443 134 | self.username = 'admin' 135 | self.password = 'admin' 136 | self.client = PyPowerFlex.PowerFlexClient(self.gateway_address, 137 | self.gateway_port, 138 | self.username, 139 | self.password, 140 | log_level=logging.DEBUG) 141 | requests.request = self.get_mock_response 142 | self.get_mock = self.mock_object(requests, 143 | 'get', 144 | side_effect=self.get_mock_response) 145 | self.post_mock = self.mock_object(requests, 146 | 'post', 147 | side_effect=self.get_mock_response) 148 | utils.is_version_3 = mock.MagicMock(return_value=True) 149 | 150 | def mock_object(self, obj, attr_name, *args, **kwargs): 151 | """Use python mock to mock an object attribute. 152 | 153 | Mocks the specified objects attribute with the given value. 154 | Automatically performs 'addCleanup' for the mock. 155 | """ 156 | patcher = mock.patch.object(obj, attr_name, *args, **kwargs) 157 | result = patcher.start() 158 | self.addCleanup(patcher.stop) 159 | return result 160 | 161 | @contextlib.contextmanager 162 | def http_response_mode(self, mode): 163 | """ 164 | Context manager for setting the HTTP response mode. 165 | 166 | Args: 167 | mode: The HTTP response mode. 168 | 169 | Yields: 170 | None. 171 | """ 172 | previous_response_mode, self.__http_response_mode = ( 173 | self.__http_response_mode, mode 174 | ) 175 | yield 176 | self.__http_response_mode = previous_response_mode 177 | 178 | def get_mock_response(self, url, request_url=None, mode=None, *args, **kwargs): 179 | """ 180 | Get a mock HTTP response. 181 | 182 | Args: 183 | url (str): The URL of the request. 184 | request_url (str): The URL of the request. 185 | mode (str): The HTTP response mode. 186 | *args: Additional arguments. 187 | **kwargs: Additional keyword arguments. 188 | 189 | Returns: 190 | requests.Response: The mocked HTTP response. 191 | """ 192 | if mode is None: 193 | mode = self.__http_response_mode 194 | 195 | api_path = url.split('/api')[1] if ('/api' in url) else request_url.split('/api')[1] 196 | try: 197 | if api_path == "/login": 198 | response = self.RESPONSE_MODE.Valid[0] 199 | elif api_path == "/logout": 200 | response = self.RESPONSE_MODE.Valid[2] 201 | else: 202 | response = self.MOCK_RESPONSES[mode][api_path] 203 | except KeyError as e: 204 | try: 205 | response = self.DEFAULT_MOCK_RESPONSES[mode][api_path] 206 | except KeyError: 207 | if mode == self.RESPONSE_MODE.BadStatus: 208 | response = self.BAD_STATUS_RESPONSE 209 | else: 210 | raise Exception( 211 | f"Mock API Endpoint is not implemented: [{mode}]" 212 | f"{api_path}" 213 | ) from e 214 | if not isinstance(response, MockResponse): 215 | response = self._get_mock_response(response) 216 | 217 | response.request.url = url 218 | response.request.body = kwargs.get('data') 219 | return response 220 | 221 | def _get_mock_response(self, response): 222 | """ 223 | Returns a MockResponse object based on the given response. 224 | 225 | Args: 226 | response (str): The response to be wrapped. 227 | 228 | Returns: 229 | MockResponse: The mock response object. 230 | """ 231 | if "204" in str(response): 232 | return MockResponse(response, 204) 233 | return MockResponse(response, 200) 234 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | testtools 2 | pytest 3 | pytest-coverage 4 | 5 | -------------------------------------------------------------------------------- /tests/test_acceleration_pool.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for testing accelaration pool client.""" 17 | 18 | # pylint: disable=invalid-name 19 | 20 | from PyPowerFlex import exceptions 21 | from PyPowerFlex.objects import acceleration_pool 22 | import tests 23 | 24 | 25 | class TestAccelerationPoolClient(tests.PyPowerFlexTestCase): 26 | """ 27 | Test class for the AccelerationPoolClient. 28 | """ 29 | def setUp(self): 30 | """ 31 | Set up the test environment. 32 | """ 33 | super().setUp() 34 | self.client.initialize() 35 | self.fake_pd_id = '1' 36 | self.fake_ap_id = '1' 37 | self.fake_device_id = '1' 38 | 39 | self.MOCK_RESPONSES = { 40 | self.RESPONSE_MODE.Valid: { 41 | '/types/AccelerationPool/instances': 42 | {'id': self.fake_ap_id}, 43 | f'/instances/AccelerationPool::{self.fake_ap_id}': 44 | {'id': self.fake_ap_id}, 45 | f'/instances/AccelerationPool::{self.fake_ap_id}/action/removeAccelerationPool': 46 | {}, 47 | '/types/AccelerationPool' 48 | '/instances/action/querySelectedStatistics': { 49 | self.fake_ap_id: {'accelerationDeviceIds': [self.fake_device_id]} 50 | }, 51 | }, 52 | self.RESPONSE_MODE.Invalid: { 53 | '/types/AccelerationPool/instances': 54 | {}, 55 | } 56 | } 57 | 58 | def test_acceleration_pool_create(self): 59 | """ 60 | Test the create method of the AccelerationPoolClient. 61 | """ 62 | self.client.acceleration_pool.create( 63 | media_type=acceleration_pool.MediaType.ssd, 64 | protection_domain_id=self.fake_pd_id, 65 | is_rfcache=True) 66 | 67 | def test_acceleration_pool_create_bad_status(self): 68 | """ 69 | Test the create method of the AccelerationPoolClient with a bad status. 70 | """ 71 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 72 | self.assertRaises(exceptions.PowerFlexFailCreating, 73 | self.client.acceleration_pool.create, 74 | media_type=acceleration_pool.MediaType.ssd, 75 | protection_domain_id=self.fake_pd_id, 76 | is_rfcache=True) 77 | 78 | def test_acceleration_pool_create_no_id_in_response(self): 79 | """ 80 | Test the create method of the AccelerationPoolClient with no id in the response. 81 | """ 82 | with self.http_response_mode(self.RESPONSE_MODE.Invalid): 83 | self.assertRaises(KeyError, 84 | self.client.acceleration_pool.create, 85 | media_type=acceleration_pool.MediaType.ssd, 86 | protection_domain_id=self.fake_pd_id, 87 | is_rfcache=True) 88 | 89 | def test_acceleration_pool_delete(self): 90 | """ 91 | Test the delete method of the AccelerationPoolClient. 92 | """ 93 | self.client.acceleration_pool.delete(self.fake_ap_id) 94 | 95 | def test_acceleration_pool_delete_bad_status(self): 96 | """ 97 | Test the delete method of the AccelerationPoolClient with a bad status. 98 | """ 99 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 100 | self.assertRaises(exceptions.PowerFlexFailDeleting, 101 | self.client.acceleration_pool.delete, 102 | self.fake_ap_id) 103 | 104 | def test_acceleration_pool_query_selected_statistics(self): 105 | """ 106 | Test the query_selected_statistics method of the AccelerationPoolClient. 107 | """ 108 | ret = self.client.acceleration_pool.query_selected_statistics( 109 | properties=["accelerationDeviceIds"] 110 | ) 111 | assert ret.get(self.fake_ap_id).get("accelerationDeviceIds") == [ 112 | self.fake_device_id 113 | ] 114 | 115 | def test_acceleration_pool_query_selected_statistics_bad_status(self): 116 | """ 117 | Test the query_selected_statistics method of the AccelerationPoolClient with a bad status. 118 | """ 119 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 120 | self.assertRaises( 121 | exceptions.PowerFlexFailQuerying, 122 | self.client.acceleration_pool.query_selected_statistics, 123 | properties=["accelerationDeviceIds"], 124 | ) 125 | -------------------------------------------------------------------------------- /tests/test_base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for testing base client.""" 17 | 18 | import json 19 | 20 | from PyPowerFlex import exceptions 21 | from PyPowerFlex import utils 22 | import tests 23 | 24 | 25 | class TestBaseClient(tests.PyPowerFlexTestCase): 26 | """ 27 | Test class for the BaseClient. 28 | """ 29 | def setUp(self): 30 | """ 31 | Set up the test environment. 32 | """ 33 | super().setUp() 34 | self.fake_response = [ 35 | { 36 | 'first': 1, 37 | 'second': 1024, 38 | 'third': ['one', 'two'] 39 | }, 40 | { 41 | 'first': 2, 42 | 'second': 2048, 43 | 'third': ['two', 'three'] 44 | } 45 | ] 46 | 47 | def test_client_not_initialized(self): 48 | """ 49 | Test that the client is not initialized. 50 | """ 51 | with self.assertRaises(exceptions.ClientNotInitialized): 52 | self.client.volume.get() 53 | 54 | def test_client_initialize(self): 55 | """ 56 | Test that the client is initialized. 57 | """ 58 | self.client.initialize() 59 | 60 | def test_client_initialize_required_params_not_set(self): 61 | """ 62 | Test that the client initialization fails when required parameters are not set. 63 | """ 64 | self.client.configuration.gateway_address = None 65 | with self.assertRaises(exceptions.InvalidConfiguration): 66 | self.client.initialize() 67 | 68 | def test_client_initialize_api_version_not_supported(self): 69 | """ 70 | Test that the client initialization fails when the API version is not supported. 71 | """ 72 | with self.http_response_mode(self.RESPONSE_MODE.Invalid): 73 | with self.assertRaises(exceptions.PowerFlexClientException): 74 | self.client.initialize() 75 | 76 | def test_utils_filter_response(self): 77 | """ 78 | Test that the response is filtered correctly. 79 | """ 80 | filter_fields = {'second': 2048} 81 | result = utils.filter_response(self.fake_response, filter_fields) 82 | self.assertTrue(len(result) == 1) 83 | self.assertTrue(result[0]['second'] == 2048) 84 | 85 | def test_utils_filter_response_iterable_in_response(self): 86 | """ 87 | Test that the response is filtered correctly when the filter field is an iterable. 88 | """ 89 | filter_fields = {'third': 'one'} 90 | result = utils.filter_response(self.fake_response, filter_fields) 91 | self.assertTrue(len(result) == 1) 92 | 93 | def test_utils_filter_response_iterable_filter_field(self): 94 | """ 95 | Test that the response is filtered correctly when the filter field is an iterable. 96 | """ 97 | filter_fields = {'third': ['one', 'two']} 98 | result = utils.filter_response(self.fake_response, filter_fields) 99 | self.assertTrue(len(result) == 2) 100 | 101 | def test_utils_filter_response_iterable_filter_field_no_match(self): 102 | """ 103 | Test that the response is filtered correctly when the filter field is an iterable 104 | and has no match. 105 | """ 106 | filter_fields = {'third': ['four', 'five']} 107 | result = utils.filter_response(self.fake_response, filter_fields) 108 | self.assertFalse(len(result)) 109 | 110 | def test_utils_filter_response_invalid_field(self): 111 | """ 112 | Test that the response is filtered correctly when the filter field is not found. 113 | """ 114 | filter_fields = {'not_found_in_response': True} 115 | result = utils.filter_response(self.fake_response, filter_fields) 116 | self.assertTrue(len(result) == 0) 117 | 118 | def test_utils_query_response_fields_list(self): 119 | """ 120 | Test that the response fields are queried correctly. 121 | """ 122 | fields = ('first',) 123 | result = utils.query_response_fields(self.fake_response, fields) 124 | self.assertTrue(all(map(lambda entity: len(entity) == 1, result))) 125 | self.assertTrue(all(map(lambda entity: entity['first'], result))) 126 | 127 | def test_utils_query_response_fields_dict(self): 128 | """ 129 | Test that the response fields are queried correctly. 130 | """ 131 | fields = ('first',) 132 | result = utils.query_response_fields(self.fake_response[0], fields) 133 | self.assertTrue(len(result) == 1) 134 | self.assertTrue(result['first']) 135 | 136 | def test_utils_query_response_fields_invalid_field(self): 137 | """ 138 | Test that the response fields are queried correctly with invalid field. 139 | """ 140 | fields = ('not_found_in_response', 'first', 'second') 141 | with self.assertRaises(exceptions.FieldsNotFound): 142 | utils.query_response_fields(self.fake_response, fields) 143 | 144 | def test_utils_prepare_params(self): 145 | """ 146 | Test that the parameters are prepared correctly. 147 | """ 148 | params = {"first": 1, "second": True, "third": None} 149 | prepared = json.loads(utils.prepare_params(params)) 150 | self.assertNotIn('third', prepared) 151 | self.assertEqual('1', prepared['first']) 152 | self.assertEqual('True', prepared['second']) 153 | 154 | def test_utils_prepare_params_with_lists(self): 155 | """ 156 | Test that the parameters are prepared correctly when the values are lists. 157 | """ 158 | params = {"first": ["second", 3, [4, True, {"fifth": 5}]]} 159 | prepared = json.loads(utils.prepare_params(params)) 160 | self.assertEqual( 161 | {'first': ['second', '3', ['4', 'True', {'fifth': 5}]]}, prepared 162 | ) 163 | -------------------------------------------------------------------------------- /tests/test_deployment.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for testing deployment client.""" 17 | 18 | # pylint: disable=invalid-name 19 | 20 | from PyPowerFlex import exceptions 21 | import tests 22 | 23 | 24 | class TestDeploymentClient(tests.PyPowerFlexTestCase): 25 | """ 26 | Test class for the DeploymentClient. 27 | """ 28 | def setUp(self): 29 | """ 30 | Set up the test environment. 31 | """ 32 | super().setUp() 33 | self.client.initialize() 34 | self.RESPONSE_204 = "" 35 | self.deployment_id = '8aaa03a88de961fa018de9c882d20301' 36 | self.rg_data = {} 37 | self.MOCK_RESPONSES = { 38 | self.RESPONSE_MODE.Valid: { 39 | '/V1/Deployment': {}, 40 | '/V1/Deployment?filter=co,name,Partial&includeDevices=False': {}, 41 | f'/V1/Deployment/{self.deployment_id}': {}, 42 | '/V1/Deployment/validate': {} 43 | } 44 | } 45 | 46 | def test_deployment_get(self): 47 | """ 48 | Test the get method of the DeploymentClient. 49 | """ 50 | self.client.deployment.get() 51 | 52 | def test_deployment_get_with_query_params(self): 53 | """ 54 | Test the get method of the DeploymentClient with query parameters. 55 | """ 56 | self.client.deployment.get(filters=['co,name,Partial'], include_devices=False) 57 | 58 | def test_deployment_get_by_id(self): 59 | """ 60 | Test the get_by_id method of the DeploymentClient. 61 | """ 62 | self.client.deployment.get_by_id(self.deployment_id) 63 | 64 | def test_deployment_get_bad_status(self): 65 | """ 66 | Test the get method of the DeploymentClient with a bad status. 67 | """ 68 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 69 | self.assertRaises(exceptions.PowerFlexClientException, 70 | self.client.deployment.get) 71 | 72 | def test_deployment_get_by_id_bad_status(self): 73 | """ 74 | Test the get_by_id method of the DeploymentClient with a bad status. 75 | """ 76 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 77 | self.assertRaises(exceptions.PowerFlexClientException, 78 | self.client.deployment.get_by_id, 79 | self.deployment_id) 80 | 81 | def test_deployment_create(self): 82 | """ 83 | Test the create method of the DeploymentClient. 84 | """ 85 | self.client.deployment.create(self.rg_data) 86 | 87 | def test_deployment_create_bad_status(self): 88 | """ 89 | Test the create method of the DeploymentClient with a bad status. 90 | """ 91 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 92 | self.assertRaises(exceptions.PowerFlexClientException, 93 | self.client.deployment.create, 94 | self.rg_data) 95 | 96 | def test_deployment_edit(self): 97 | """ 98 | Test the edit method of the DeploymentClient. 99 | """ 100 | self.client.deployment.edit(self.deployment_id, self.rg_data) 101 | 102 | def test_deployment_edit_bad_status(self): 103 | """ 104 | Test the edit method of the DeploymentClient with a bad status. 105 | """ 106 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 107 | self.assertRaises(exceptions.PowerFlexClientException, 108 | self.client.deployment.edit, 109 | self.deployment_id, 110 | self.rg_data) 111 | 112 | def test_deployment_delete(self): 113 | """ 114 | Test the delete method of the DeploymentClient. 115 | """ 116 | url = f'/V1/Deployment/{self.deployment_id}' 117 | self.MOCK_RESPONSES[self.RESPONSE_MODE.Valid][url] = self.RESPONSE_204 118 | self.client.deployment.delete(self.deployment_id) 119 | 120 | def test_deployment_delete_bad_status(self): 121 | """ 122 | Test the delete method of the DeploymentClient with a bad status. 123 | """ 124 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 125 | self.assertRaises(exceptions.PowerFlexClientException, 126 | self.client.deployment.delete, 127 | self.deployment_id) 128 | 129 | def test_deployment_validate(self): 130 | """ 131 | Test the validate method of the DeploymentClient. 132 | """ 133 | self.client.deployment.validate(self.rg_data) 134 | 135 | def test_deployment_validate_bad_status(self): 136 | """ 137 | Test the validate method of the DeploymentClient with a bad status. 138 | """ 139 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 140 | self.assertRaises(exceptions.PowerFlexClientException, 141 | self.client.deployment.validate, 142 | self.rg_data) 143 | -------------------------------------------------------------------------------- /tests/test_device.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for testing device client.""" 17 | 18 | # pylint: disable=invalid-name 19 | 20 | from PyPowerFlex import exceptions 21 | from PyPowerFlex.objects.device import MediaType 22 | import tests 23 | 24 | 25 | class TestDeviceClient(tests.PyPowerFlexTestCase): 26 | """ 27 | Test class for DeviceClient. 28 | """ 29 | def setUp(self): 30 | """ 31 | Set up the test environment. 32 | """ 33 | super().setUp() 34 | self.client.initialize() 35 | self.fake_device_id = '1' 36 | self.fake_sds_id = '1' 37 | self.fake_sp_id = '1' 38 | self.fake_accp_id = '1' 39 | 40 | self.MOCK_RESPONSES = { 41 | self.RESPONSE_MODE.Valid: { 42 | '/types/Device/instances': 43 | {'id': self.fake_device_id}, 44 | f'/instances/Device::{self.fake_device_id}': 45 | {'id': self.fake_device_id}, 46 | f'/instances/Device::{self.fake_device_id}' 47 | '/action/removeDevice': 48 | {}, 49 | f'/instances/Device::{self.fake_device_id}' 50 | '/action/setDeviceName': 51 | {}, 52 | f'/instances/Device::{self.fake_device_id}' 53 | '/action/setMediaType': 54 | {}, 55 | '/types/Device' 56 | '/instances/action/querySelectedStatistics': { 57 | self.fake_device_id: {'avgReadLatencyInMicrosec': 0} 58 | }, 59 | }, 60 | self.RESPONSE_MODE.Invalid: { 61 | '/types/Device/instances': 62 | {}, 63 | } 64 | } 65 | 66 | def test_device_create(self): 67 | """ 68 | Test device creation. 69 | """ 70 | self.client.device.create('/dev/sda', 71 | self.fake_sds_id, 72 | media_type=MediaType.ssd, 73 | storage_pool_id=self.fake_sp_id) 74 | 75 | def test_device_create_bad_status(self): 76 | """ 77 | Test device creation with bad status. 78 | """ 79 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 80 | self.assertRaises(exceptions.PowerFlexFailCreating, 81 | self.client.device.create, 82 | '/dev/sda', 83 | self.fake_sds_id, 84 | media_type=MediaType.ssd, 85 | storage_pool_id=self.fake_sp_id) 86 | 87 | def test_device_create_no_id_in_response(self): 88 | """ 89 | Test device creation with no id in response. 90 | """ 91 | with self.http_response_mode(self.RESPONSE_MODE.Invalid): 92 | self.assertRaises(KeyError, 93 | self.client.device.create, 94 | '/dev/sda', 95 | self.fake_sds_id, 96 | media_type=MediaType.ssd, 97 | storage_pool_id=self.fake_sp_id) 98 | 99 | def test_device_create_storage_pool_id_and_acc_pool_id_are_set(self): 100 | """ 101 | Test device creation with both storage pool id and acceleration pool id set. 102 | """ 103 | self.assertRaises(exceptions.InvalidInput, 104 | self.client.device.create, 105 | '/dev/sda', 106 | self.fake_sds_id, 107 | acceleration_pool_id=self.fake_accp_id, 108 | media_type=MediaType.ssd, 109 | storage_pool_id=self.fake_sp_id) 110 | 111 | def test_device_delete(self): 112 | """ 113 | Test device deletion. 114 | """ 115 | self.client.device.delete(self.fake_device_id) 116 | 117 | def test_device_delete_bad_status(self): 118 | """ 119 | Test device deletion with bad status. 120 | """ 121 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 122 | self.assertRaises(exceptions.PowerFlexFailDeleting, 123 | self.client.device.delete, 124 | self.fake_device_id) 125 | 126 | def test_device_rename(self): 127 | """ 128 | Test device renaming. 129 | """ 130 | self.client.device.rename(self.fake_device_id, name='new_name') 131 | 132 | def test_device_rename_bad_status(self): 133 | """ 134 | Test device renaming with bad status. 135 | """ 136 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 137 | self.assertRaises(exceptions.PowerFlexFailRenaming, 138 | self.client.device.rename, 139 | self.fake_device_id, 140 | name='new_name') 141 | 142 | def test_device_set_media_type(self): 143 | """ 144 | Test device media type setting. 145 | """ 146 | self.client.device.set_media_type( 147 | self.fake_device_id, 148 | media_type=MediaType.hdd 149 | ) 150 | 151 | def test_device_set_media_type_bad_status(self): 152 | """ 153 | Test device media type setting with bad status. 154 | """ 155 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 156 | self.assertRaises( 157 | exceptions.PowerFlexClientException, 158 | self.client.device.set_media_type, 159 | self.fake_device_id, 160 | media_type=MediaType.hdd 161 | ) 162 | 163 | def test_device_query_selected_statistics(self): 164 | """ 165 | Test device query selected statistics. 166 | """ 167 | ret = self.client.device.query_selected_statistics( 168 | properties=["avgReadLatencyInMicrosec"] 169 | ) 170 | assert ret.get(self.fake_device_id).get("avgReadLatencyInMicrosec") == 0 171 | 172 | def test_device_query_selected_statistics_bad_status(self): 173 | """ 174 | Test device query selected statistics with bad status. 175 | """ 176 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 177 | self.assertRaises( 178 | exceptions.PowerFlexFailQuerying, 179 | self.client.device.query_selected_statistics, 180 | properties=["avgReadLatencyInMicrosec"], 181 | ) 182 | -------------------------------------------------------------------------------- /tests/test_fault_set.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for testing fault set client.""" 17 | 18 | # pylint: disable=invalid-name 19 | 20 | from PyPowerFlex import exceptions 21 | import tests 22 | 23 | 24 | class TestFaultSetClient(tests.PyPowerFlexTestCase): 25 | """ 26 | Test class for the PowerFlex FaultSetClient. 27 | """ 28 | 29 | def setUp(self): 30 | """ 31 | Set up the test environment. 32 | """ 33 | super().setUp() 34 | self.client.initialize() 35 | self.fake_fault_set_id = '1' 36 | self.fake_pd_id = '1' 37 | 38 | self.MOCK_RESPONSES = { 39 | self.RESPONSE_MODE.Valid: { 40 | '/types/FaultSet/instances': 41 | {'id': self.fake_fault_set_id}, 42 | f'/instances/FaultSet::{self.fake_fault_set_id}': 43 | {'id': self.fake_fault_set_id}, 44 | f'/instances/FaultSet::{self.fake_fault_set_id}/action/clearFaultSet': 45 | {}, 46 | f'/instances/FaultSet::{self.fake_fault_set_id}/relationships/Sds': 47 | [], 48 | f'/instances/FaultSet::{self.fake_fault_set_id}/action/removeFaultSet': 49 | {}, 50 | f'/instances/FaultSet::{self.fake_fault_set_id}/action/setFaultSetName': 51 | {}, 52 | '/types/FaultSet' 53 | '/instances/action/querySelectedStatistics': { 54 | self.fake_fault_set_id: {'rfcacheFdReadTimeGreater5Sec': 0} 55 | }, 56 | }, 57 | self.RESPONSE_MODE.Invalid: { 58 | '/types/FaultSet/instances': 59 | {}, 60 | } 61 | } 62 | 63 | def test_fault_set_clear(self): 64 | """ 65 | Test clearing a fault set. 66 | """ 67 | self.client.fault_set.clear(self.fake_fault_set_id) 68 | 69 | def test_fault_set_clear_bad_status(self): 70 | """ 71 | Test clearing a fault set with a bad status. 72 | """ 73 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 74 | self.assertRaises(exceptions.PowerFlexClientException, 75 | self.client.fault_set.clear, 76 | self.fake_fault_set_id) 77 | 78 | def test_fault_set_create(self): 79 | """ 80 | Test creating a fault set. 81 | """ 82 | self.client.fault_set.create(self.fake_pd_id, name='fake_name') 83 | 84 | def test_fault_set_create_bad_status(self): 85 | """ 86 | Test creating a fault set with a bad status. 87 | """ 88 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 89 | self.assertRaises(exceptions.PowerFlexFailCreating, 90 | self.client.fault_set.create, 91 | self.fake_pd_id, 92 | name='fake_name') 93 | 94 | def test_fault_set_create_no_id_in_response(self): 95 | """ 96 | Test creating a fault set with no ID in the response. 97 | """ 98 | with self.http_response_mode(self.RESPONSE_MODE.Invalid): 99 | self.assertRaises(KeyError, 100 | self.client.fault_set.create, 101 | self.fake_pd_id, 102 | name='fake_name') 103 | 104 | def test_fault_set_get_sdss(self): 105 | """ 106 | Test getting the SDS for a fault set. 107 | """ 108 | self.client.fault_set.get_sdss(self.fake_fault_set_id) 109 | 110 | def test_fault_set_get_sdss_bad_status(self): 111 | """ 112 | Test getting the SDS for a fault set with a bad status. 113 | """ 114 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 115 | self.assertRaises(exceptions.PowerFlexClientException, 116 | self.client.fault_set.get_sdss, 117 | self.fake_fault_set_id) 118 | 119 | def test_fault_set_delete(self): 120 | """ 121 | Test deleting a fault set. 122 | """ 123 | self.client.fault_set.delete(self.fake_fault_set_id) 124 | 125 | def test_fault_set_delete_bad_status(self): 126 | """ 127 | Test deleting a fault set with a bad status. 128 | """ 129 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 130 | self.assertRaises(exceptions.PowerFlexFailDeleting, 131 | self.client.fault_set.delete, 132 | self.fake_fault_set_id) 133 | 134 | def test_fault_set_rename(self): 135 | """ 136 | Test renaming a fault set. 137 | """ 138 | self.client.fault_set.rename(self.fake_fault_set_id, name='new_name') 139 | 140 | def test_fault_set_rename_bad_status(self): 141 | """ 142 | Test renaming a fault set with a bad status. 143 | """ 144 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 145 | self.assertRaises(exceptions.PowerFlexFailRenaming, 146 | self.client.fault_set.rename, 147 | self.fake_fault_set_id, 148 | name='new_name') 149 | 150 | def test_fault_set_query_selected_statistics(self): 151 | """ 152 | Test querying selected statistics for a fault set. 153 | """ 154 | ret = self.client.fault_set.query_selected_statistics( 155 | properties=["rfcacheFdReadTimeGreater5Sec"] 156 | ) 157 | assert ret.get(self.fake_fault_set_id).get("rfcacheFdReadTimeGreater5Sec") == 0 158 | 159 | def test_fault_set_query_selected_statistics_bad_status(self): 160 | """ 161 | Test querying selected statistics for a fault set with a bad status. 162 | """ 163 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 164 | self.assertRaises( 165 | exceptions.PowerFlexFailQuerying, 166 | self.client.fault_set.query_selected_statistics, 167 | properties=["rfcacheFdReadTimeGreater5Sec"], 168 | ) 169 | -------------------------------------------------------------------------------- /tests/test_firmware_repository.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for testing firmware repository client.""" 17 | 18 | # pylint: disable=invalid-name 19 | 20 | from PyPowerFlex import exceptions 21 | import tests 22 | 23 | 24 | class TestFirmwareRepositoryClient(tests.PyPowerFlexTestCase): 25 | """ 26 | Test class for FirmwareRepositoryClient. 27 | """ 28 | def setUp(self): 29 | """ 30 | Set up the test environment. 31 | """ 32 | super().setUp() 33 | self.client.initialize() 34 | 35 | self.MOCK_RESPONSES = { 36 | self.RESPONSE_MODE.Valid: { 37 | '/V1/FirmwareRepository': {}, 38 | '/V1/FirmwareRepository?related=False&bundles=False&components=False': {} 39 | } 40 | } 41 | 42 | def test_firmware_repository_get(self): 43 | """ 44 | Test the get method of the FirmwareRepositoryClient. 45 | """ 46 | self.client.firmware_repository.get() 47 | 48 | def test_firmware_repository_get_with_query_params(self): 49 | """ 50 | Test the get method of the FirmwareRepositoryClient with query parameters. 51 | """ 52 | self.client.firmware_repository.get(related=False, bundles=False, components=False) 53 | 54 | def test_firmware_repository_get_bad_status(self): 55 | """ 56 | Test the get method of the FirmwareRepositoryClient with a bad status. 57 | """ 58 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 59 | self.assertRaises(exceptions.PowerFlexClientException, 60 | self.client.firmware_repository.get) 61 | -------------------------------------------------------------------------------- /tests/test_host.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for testing host client.""" 17 | 18 | # pylint: disable=invalid-name 19 | 20 | from PyPowerFlex import exceptions 21 | import tests 22 | 23 | 24 | class TestHostClient(tests.PyPowerFlexTestCase): 25 | """ 26 | Tests for the HostClient class. 27 | """ 28 | def setUp(self): 29 | """ 30 | Set up the test environment. 31 | """ 32 | super().setUp() 33 | self.client.initialize() 34 | self.fake_host_id="1" 35 | self.fake_nqn = "nqn::" 36 | 37 | self.MOCK_RESPONSES = { 38 | self.RESPONSE_MODE.Valid: { 39 | # create 40 | '/types/Host/instances': {'id': self.fake_host_id}, 41 | f'/instances/Host::{self.fake_host_id}': 42 | {'id': self.fake_host_id}, 43 | f'/instances/Host::{self.fake_host_id}/action/modifyMaxNumPaths': {}, 44 | f'/instances/Host::{self.fake_host_id}/action/modifyMaxNumSysPorts': {}, 45 | } 46 | } 47 | 48 | def test_sdc_host_create(self): 49 | """ 50 | Test the creation of a new host. 51 | """ 52 | self.client.host.create(self.fake_nqn, max_num_paths='8', max_num_sys_ports='8') 53 | 54 | def test_sdc_host_create_bad_status(self): 55 | """ 56 | Test the creation of a new host with a bad status. 57 | """ 58 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 59 | self.assertRaises(exceptions.PowerFlexFailCreating, 60 | self.client.host.create, self.fake_nqn) 61 | def test_sdc_modify_max_num_paths(self): 62 | """ 63 | Test the modification of the maximum number of paths. 64 | """ 65 | self.client.host.modify_max_num_paths(self.fake_host_id, max_num_paths='8') 66 | 67 | def test_sdc_modify_max_num_paths_bad_status(self): 68 | """ 69 | Test the modification of the maximum number of paths with a bad status. 70 | """ 71 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 72 | self.assertRaises(exceptions.PowerFlexFailEntityOperation, 73 | self.client.host.modify_max_num_paths, 74 | self.fake_host_id, 75 | max_num_paths='8') 76 | 77 | def test_sdc_modify_max_num_sys_ports(self): 78 | """ 79 | Test the modification of the maximum number of system ports. 80 | """ 81 | self.client.host.modify_max_num_sys_ports(self.fake_host_id, max_num_sys_ports='8') 82 | 83 | def test_sdc_modify_max_num_sys_ports_bad_status(self): 84 | """ 85 | Test the modification of the maximum number of system ports with a bad status. 86 | """ 87 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 88 | self.assertRaises(exceptions.PowerFlexFailEntityOperation, 89 | self.client.host.modify_max_num_sys_ports, 90 | self.fake_host_id, 91 | max_num_sys_ports='8') 92 | -------------------------------------------------------------------------------- /tests/test_managed_device.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for testing managed device client.""" 17 | 18 | # pylint: disable=invalid-name 19 | 20 | from PyPowerFlex import exceptions 21 | import tests 22 | 23 | 24 | class TestManagedDeviceClient(tests.PyPowerFlexTestCase): 25 | """ 26 | Test class for the ManagedDeviceClient. 27 | """ 28 | def setUp(self): 29 | """ 30 | Set up the test environment. 31 | """ 32 | super().setUp() 33 | self.client.initialize() 34 | 35 | self.MOCK_RESPONSES = { 36 | self.RESPONSE_MODE.Valid: { 37 | '/V1/ManagedDevice': {}, 38 | '/V1/ManagedDevice?filter=eq,deviceType,scaleio&sort=state': {} 39 | } 40 | } 41 | 42 | def test_managed_device_get(self): 43 | """ 44 | Test the managed_device.get() method. 45 | """ 46 | self.client.managed_device.get() 47 | 48 | def test_managed_device_get_with_query_params(self): 49 | """ 50 | Test the managed_device.get() method with query parameters. 51 | """ 52 | self.client.managed_device.get(filters=['eq,deviceType,scaleio'], sort="state") 53 | 54 | def test_managed_device_get_bad_status(self): 55 | """ 56 | Test the managed_device.get() method with a bad status. 57 | """ 58 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 59 | self.assertRaises(exceptions.PowerFlexClientException, 60 | self.client.managed_device.get) 61 | -------------------------------------------------------------------------------- /tests/test_replication_pair.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for testing replication pair client.""" 17 | 18 | # pylint: disable=invalid-name 19 | 20 | from PyPowerFlex import exceptions 21 | import tests 22 | 23 | 24 | class TestReplicationPairClient(tests.PyPowerFlexTestCase): 25 | """ 26 | Test class for the ReplicationPairClient. 27 | """ 28 | def setUp(self): 29 | """ 30 | Set up the test environment. 31 | """ 32 | super().setUp() 33 | self.client.initialize() 34 | self.fake_replication_pair_id = '1' 35 | 36 | self.MOCK_RESPONSES = { 37 | self.RESPONSE_MODE.Valid: { 38 | '/types/ReplicationPair/instances': 39 | {'id': self.fake_replication_pair_id}, 40 | f'/instances/ReplicationPair::{self.fake_replication_pair_id}': 41 | {'id': self.fake_replication_pair_id}, 42 | f'/instances/ReplicationPair::{self.fake_replication_pair_id}' 43 | '/action/removeReplicationPair': 44 | {}, 45 | f'/instances/ReplicationPair::{self.fake_replication_pair_id}' 46 | '/action/pausePairInitialCopy': 47 | {'id': self.fake_replication_pair_id}, 48 | f'/instances/ReplicationPair::{self.fake_replication_pair_id}' 49 | '/action/resumePairInitialCopy': 50 | {'id': self.fake_replication_pair_id}, 51 | '/types/ReplicationPair' 52 | '/instances/action/querySelectedStatistics': { 53 | self.fake_replication_pair_id: {'initialCopyProgress': 0} 54 | }, 55 | }, 56 | self.RESPONSE_MODE.Invalid: { 57 | '/types/ReplicationPair/instances': 58 | {}, 59 | } 60 | } 61 | 62 | def test_add_replication_pair(self): 63 | """ 64 | Test the add method of the ReplicationPairClient. 65 | """ 66 | self.client.replication_pair.add\ 67 | (source_vol_id='1', dest_vol_id='1', 68 | rcg_id='1', copy_type='OnlineCopy', name='test') 69 | 70 | def test_remove_replication_pair(self): 71 | """ 72 | Test the remove method of the ReplicationPairClient. 73 | """ 74 | self.client.replication_pair.remove(self.fake_replication_pair_id) 75 | 76 | def test_pause_online_copy(self): 77 | """ 78 | Test the pause method of the ReplicationPairClient. 79 | """ 80 | self.client.replication_pair.pause(self.fake_replication_pair_id) 81 | 82 | def test_resume_online_copy(self): 83 | """ 84 | Test the resume method of the ReplicationPairClient. 85 | """ 86 | self.client.replication_pair.resume(self.fake_replication_pair_id) 87 | 88 | def test_get_all_statistics(self): 89 | """ 90 | Test the get_all_statistics method of the ReplicationPairClient. 91 | """ 92 | self.client.replication_pair.get_all_statistics() 93 | 94 | def test_add_replication_pair_bad_status(self): 95 | """ 96 | Test the add method of the ReplicationPairClient with a bad status. 97 | """ 98 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 99 | self.assertRaises(exceptions.PowerFlexFailCreating, 100 | self.client.replication_pair.add, 101 | source_vol_id='1', dest_vol_id='1', 102 | rcg_id='1', copy_type='OnlineCopy', name='test') 103 | 104 | def test_remove_replication_pair_bad_status(self): 105 | """ 106 | Test the remove method of the ReplicationPairClient with a bad status. 107 | """ 108 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 109 | self.assertRaises(exceptions.PowerFlexFailDeleting, 110 | self.client.replication_pair.remove, 111 | self.fake_replication_pair_id) 112 | 113 | def test_get_all_statistics_bad_status(self): 114 | """ 115 | Test the get_all_statistics method of the ReplicationPairClient with a bad status. 116 | """ 117 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 118 | self.assertRaises(exceptions.PowerFlexClientException, 119 | self.client.replication_pair.get_all_statistics) 120 | 121 | def test_replication_pair_query_selected_statistics(self): 122 | """ 123 | Test the query_selected_statistics method of the ReplicationPairClient. 124 | """ 125 | ret = self.client.replication_pair.query_selected_statistics( 126 | properties=["initialCopyProgress"] 127 | ) 128 | assert ret.get(self.fake_replication_pair_id).get("initialCopyProgress") == 0 129 | 130 | def test_replication_pair_query_selected_statistics_bad_status(self): 131 | """ 132 | Test the query_selected_statistics method of the ReplicationPairClient with a bad status. 133 | """ 134 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 135 | self.assertRaises( 136 | exceptions.PowerFlexFailQuerying, 137 | self.client.replication_pair.query_selected_statistics, 138 | properties=["initialCopyProgress"], 139 | ) 140 | -------------------------------------------------------------------------------- /tests/test_sdc.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for testing SDC client.""" 17 | 18 | # pylint: disable=invalid-name 19 | 20 | from PyPowerFlex import exceptions 21 | import tests 22 | 23 | 24 | class TestSdcClient(tests.PyPowerFlexTestCase): 25 | """ 26 | Tests for the SdcClient class. 27 | """ 28 | def setUp(self): 29 | """ 30 | Set up the test case. 31 | """ 32 | super().setUp() 33 | self.client.initialize() 34 | self.fake_sdc_id = '1' 35 | 36 | self.MOCK_RESPONSES = { 37 | self.RESPONSE_MODE.Valid: { 38 | '/types/Sdc/instances': 39 | {'id': self.fake_sdc_id}, 40 | f'/instances/Sdc::{self.fake_sdc_id}': 41 | {'id': self.fake_sdc_id}, 42 | f'/instances/Sdc::{self.fake_sdc_id}/action/removeSdc': 43 | {}, 44 | f'/instances/Sdc::{self.fake_sdc_id}/relationships/Volume': 45 | [], 46 | f'/instances/Sdc::{self.fake_sdc_id}/action/setSdcName': 47 | {}, 48 | f'/instances/Sdc::{self.fake_sdc_id}/action/setSdcPerformanceParameters': 49 | {}, 50 | '/types/Sdc' 51 | '/instances/action/querySelectedStatistics': { 52 | self.fake_sdc_id: {'numOfMappedVolumes': 1} 53 | }, 54 | } 55 | } 56 | 57 | def test_sdc_delete(self): 58 | """ 59 | Test the delete method of the SdcClient. 60 | """ 61 | self.client.sdc.delete(self.fake_sdc_id) 62 | 63 | def test_sdc_delete_bad_status(self): 64 | """ 65 | Test the delete method of the SdcClient with a bad status. 66 | """ 67 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 68 | self.assertRaises(exceptions.PowerFlexFailDeleting, 69 | self.client.sdc.delete, 70 | self.fake_sdc_id) 71 | 72 | def test_sdc_get_mapped_volumes(self): 73 | """ 74 | Test the get_mapped_volumes method of the SdcClient. 75 | """ 76 | self.client.sdc.get_mapped_volumes(self.fake_sdc_id) 77 | 78 | def test_sdc_get_mapped_volumes_bad_status(self): 79 | """ 80 | Test the get_mapped_volumes method of the SdcClient with a bad status. 81 | """ 82 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 83 | self.assertRaises(exceptions.PowerFlexClientException, 84 | self.client.sdc.get_mapped_volumes, 85 | self.fake_sdc_id) 86 | 87 | def test_sdc_rename(self): 88 | """ 89 | Test the rename method of the SdcClient. 90 | """ 91 | self.client.sdc.rename(self.fake_sdc_id, name='new_name') 92 | 93 | def test_sdc_rename_bad_status(self): 94 | """ 95 | Test the rename method of the SdcClient with a bad status. 96 | """ 97 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 98 | self.assertRaises(exceptions.PowerFlexFailRenaming, 99 | self.client.sdc.rename, 100 | self.fake_sdc_id, 101 | name='new_name') 102 | 103 | def test_set_performance_profile(self): 104 | """ 105 | Test the set_performance_profile method of the SdcClient. 106 | """ 107 | self.client.sdc.set_performance_profile(self.fake_sdc_id, 'Compact') 108 | 109 | def test_set_performance_profile_bad_status(self): 110 | """ 111 | Test the set_performance_profile method of the SdcClient with a bad status. 112 | """ 113 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 114 | self.assertRaises(exceptions.PowerFlexFailEntityOperation, 115 | self.client.sdc.set_performance_profile, 116 | self.fake_sdc_id, 117 | 'Compact') 118 | 119 | def test_sdc_query_selected_statistics(self): 120 | """ 121 | Test the query_selected_statistics method of the SdcClient. 122 | """ 123 | ret = self.client.sdc.query_selected_statistics( 124 | properties=["numOfMappedVolumes"] 125 | ) 126 | assert ret.get(self.fake_sdc_id).get("numOfMappedVolumes") == 1 127 | 128 | def test_sdc_query_selected_statistics_bad_status(self): 129 | """ 130 | Test the query_selected_statistics method of the SdcClient with a bad status. 131 | """ 132 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 133 | self.assertRaises( 134 | exceptions.PowerFlexFailQuerying, 135 | self.client.sdc.query_selected_statistics, 136 | properties=["numOfMappedVolumes"], 137 | ) 138 | -------------------------------------------------------------------------------- /tests/test_sdt.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for testing SDT client.""" 17 | 18 | # pylint: disable=invalid-name,too-many-public-methods 19 | 20 | from PyPowerFlex import exceptions 21 | from PyPowerFlex.objects import sdt 22 | import tests 23 | 24 | 25 | class TestSdtClient(tests.PyPowerFlexTestCase): 26 | """ 27 | Tests for the SdtClient class. 28 | """ 29 | def setUp(self): 30 | """ 31 | Set up the test environment. 32 | """ 33 | super().setUp() 34 | self.client.initialize() 35 | self.fake_sdt_id = "1" 36 | self.fake_sdt_name = "1" 37 | self.fake_pd_id = "1" 38 | self.fake_sdt_ips = [sdt.SdtIp("1.2.3.4", sdt.SdtIpRoles.storage_and_host)] 39 | 40 | self.MOCK_RESPONSES = { 41 | self.RESPONSE_MODE.Valid: { 42 | "/types/Sdt/instances": {"id": self.fake_sdt_id}, 43 | f"/instances/Sdt::{self.fake_sdt_id}": {"id": self.fake_sdt_id}, 44 | f"/instances/Sdt::{self.fake_sdt_id}/action/addIp": {}, 45 | f"/instances/Sdt::{self.fake_sdt_id}/action/removeIp": {}, 46 | f"/instances/Sdt::{self.fake_sdt_id}/action/renameSdt": {}, 47 | f"/instances/Sdt::{self.fake_sdt_id}/action/modifyIpRole": {}, 48 | f"/instances/Sdt::{self.fake_sdt_id}/action/modifyStoragePort": {}, 49 | f"/instances/Sdt::{self.fake_sdt_id}/action/modifyNvmePort": {}, 50 | f"/instances/Sdt::{self.fake_sdt_id}/action/modifyDiscoveryPort": {}, 51 | f"/instances/Sdt::{self.fake_sdt_id}/action/enterMaintenanceMode": {}, 52 | f"/instances/Sdt::{self.fake_sdt_id}/action/exitMaintenanceMode": {}, 53 | f"/instances/Sdt::{self.fake_sdt_id}/action/removeSdt": {}, 54 | }, 55 | self.RESPONSE_MODE.Invalid: { 56 | "/types/Sdt/instances": {}, 57 | }, 58 | } 59 | 60 | def test_sdt_create(self): 61 | """ 62 | Test the create method of the SdtClient. 63 | """ 64 | self.client.sdt.create( 65 | protection_domain_id=self.fake_pd_id, 66 | sdt_ips=self.fake_sdt_ips, 67 | sdt_name=self.fake_sdt_name, 68 | ) 69 | 70 | def test_sdt_create_bad_status(self): 71 | """ 72 | Test the create method of the SdtClient with a bad status. 73 | """ 74 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 75 | self.assertRaises( 76 | exceptions.PowerFlexFailCreating, 77 | self.client.sdt.create, 78 | protection_domain_id=self.fake_pd_id, 79 | sdt_ips=self.fake_sdt_ips, 80 | sdt_name=self.fake_sdt_name, 81 | ) 82 | 83 | def test_sdt_create_no_id_in_response(self): 84 | """ 85 | Test the create method of the SdtClient with no id in the response. 86 | """ 87 | with self.http_response_mode(self.RESPONSE_MODE.Invalid): 88 | self.assertRaises( 89 | KeyError, 90 | self.client.sdt.create, 91 | protection_domain_id=self.fake_pd_id, 92 | sdt_ips=self.fake_sdt_ips, 93 | sdt_name=self.fake_sdt_name, 94 | ) 95 | 96 | def test_sdt_rename(self): 97 | """ 98 | Test the rename method of the SdtClient. 99 | """ 100 | self.client.sdt.rename(self.fake_sdt_id, name="new_name") 101 | 102 | def test_sdt_rename_bad_status(self): 103 | """ 104 | Test the rename method of the SdtClient with a bad status. 105 | """ 106 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 107 | self.assertRaises( 108 | exceptions.PowerFlexFailRenaming, 109 | self.client.sdt.rename, 110 | self.fake_sdt_id, 111 | name="new_name", 112 | ) 113 | 114 | def test_sdt_add_ip(self): 115 | """ 116 | Test the add_ip method of the SdtClient. 117 | """ 118 | self.client.sdt.add_ip( 119 | self.fake_sdt_id, ip="1.2.3.4", role=sdt.SdtIpRoles.storage_and_host 120 | ) 121 | 122 | def test_sdt_add_ip_bad_status(self): 123 | """ 124 | Test the add_ip method of the SdtClient with a bad status. 125 | """ 126 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 127 | self.assertRaises( 128 | exceptions.PowerFlexClientException, 129 | self.client.sdt.add_ip, 130 | self.fake_sdt_id, 131 | ip="1.2.3.4", 132 | role=sdt.SdtIpRoles.storage_and_host, 133 | ) 134 | 135 | def test_sdt_remove_ip(self): 136 | """ 137 | Test the remove_ip method of the SdtClient. 138 | """ 139 | self.client.sdt.remove_ip(self.fake_sdt_id, ip="1.2.3.4") 140 | 141 | def test_sdt_remove_ip_bad_status(self): 142 | """ 143 | Test the remove_ip method of the SdtClient with a bad status. 144 | """ 145 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 146 | self.assertRaises( 147 | exceptions.PowerFlexClientException, 148 | self.client.sdt.remove_ip, 149 | self.fake_sdt_id, 150 | ip="1.2.3.4", 151 | ) 152 | 153 | def test_sdt_set_ip_role(self): 154 | """ 155 | Test the set_ip_role method of the SdtClient. 156 | """ 157 | self.client.sdt.set_ip_role( 158 | self.fake_sdt_id, ip="1.2.3.4", role=sdt.SdtIpRoles.storage_and_host 159 | ) 160 | 161 | def test_sdt_set_ip_role_bad_status(self): 162 | """ 163 | Test the set_ip_role method of the SdtClient with a bad status. 164 | """ 165 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 166 | self.assertRaises( 167 | exceptions.PowerFlexClientException, 168 | self.client.sdt.set_ip_role, 169 | self.fake_sdt_id, 170 | ip="1.2.3.4", 171 | role=sdt.SdtIpRoles.storage_and_host, 172 | ) 173 | 174 | def test_sdt_set_storage_port(self): 175 | """ 176 | Test the set_storage_port method of the SdtClient. 177 | """ 178 | self.client.sdt.set_storage_port(self.fake_sdt_id, storage_port=12200) 179 | 180 | def test_sdt_set_storage_port_bad_status(self): 181 | """ 182 | Test the set_storage_port method of the SdtClient with a bad status. 183 | """ 184 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 185 | self.assertRaises( 186 | exceptions.PowerFlexClientException, 187 | self.client.sdt.set_storage_port, 188 | self.fake_sdt_id, 189 | storage_port=12200, 190 | ) 191 | 192 | def test_sdt_set_nvme_port(self): 193 | """ 194 | Test case for setting NVMe port of a Storage Device Target. 195 | """ 196 | self.client.sdt.set_nvme_port(self.fake_sdt_id, nvme_port=4420) 197 | 198 | def test_sdt_set_nvme_port_bad_status(self): 199 | """ 200 | Test case for setting NVMe port of a Storage Device Target with bad status. 201 | """ 202 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 203 | self.assertRaises( 204 | exceptions.PowerFlexClientException, 205 | self.client.sdt.set_nvme_port, 206 | self.fake_sdt_id, 207 | nvme_port=4420, 208 | ) 209 | 210 | def test_sdt_set_discovery_port(self): 211 | """ 212 | Test case for setting discovery port of a Storage Device Target. 213 | """ 214 | self.client.sdt.set_discovery_port(self.fake_sdt_id, discovery_port=8009) 215 | 216 | def test_sdt_set_discovery_port_bad_status(self): 217 | """ 218 | Test case for setting discovery port of a Storage Device Target with bad status. 219 | """ 220 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 221 | self.assertRaises( 222 | exceptions.PowerFlexClientException, 223 | self.client.sdt.set_discovery_port, 224 | self.fake_sdt_id, 225 | discovery_port=8009, 226 | ) 227 | 228 | def test_sdt_enter_maintenance_mode(self): 229 | """ 230 | Test case for entering maintenance mode of a Storage Device Target. 231 | """ 232 | self.client.sdt.enter_maintenance_mode(self.fake_sdt_id) 233 | 234 | def test_sdt_enter_maintenance_mode_bad_status(self): 235 | """ 236 | Test case for entering maintenance mode of a Storage Device Target with bad status. 237 | """ 238 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 239 | self.assertRaises( 240 | exceptions.PowerFlexClientException, 241 | self.client.sdt.enter_maintenance_mode, 242 | self.fake_sdt_id, 243 | ) 244 | 245 | def test_sdt_exit_maintenance_mode(self): 246 | """ 247 | Test case for exiting maintenance mode of a Storage Device Target. 248 | """ 249 | self.client.sdt.exit_maintenance_mode(self.fake_sdt_id) 250 | 251 | def test_sdt_exit_maintenance_mode_bad_status(self): 252 | """ 253 | Test case for exiting maintenance mode of a Storage Device Target with bad status. 254 | """ 255 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 256 | self.assertRaises( 257 | exceptions.PowerFlexClientException, 258 | self.client.sdt.exit_maintenance_mode, 259 | self.fake_sdt_id, 260 | ) 261 | 262 | def test_sdt_delete(self): 263 | """ 264 | Test case for deleting a Storage Device Target. 265 | """ 266 | self.client.sdt.delete(self.fake_sdt_id) 267 | 268 | def test_sdt_delete_bad_status(self): 269 | """ 270 | Test case for deleting a Storage Device Target with bad status. 271 | """ 272 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 273 | self.assertRaises( 274 | exceptions.PowerFlexFailDeleting, 275 | self.client.sdt.delete, 276 | self.fake_sdt_id, 277 | ) 278 | -------------------------------------------------------------------------------- /tests/test_service_template.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for testing service template client.""" 17 | 18 | # pylint: disable=invalid-name 19 | 20 | from PyPowerFlex import exceptions 21 | import tests 22 | 23 | 24 | class TestServiceTemplateClient(tests.PyPowerFlexTestCase): 25 | """ 26 | Test class for the ServiceTemplateClient. 27 | """ 28 | def setUp(self): 29 | """ 30 | Set up the test environment. 31 | """ 32 | super().setUp() 33 | self.client.initialize() 34 | self.template_id = 1234 35 | self.MOCK_RESPONSES = { 36 | self.RESPONSE_MODE.Valid: { 37 | '/V1/ServiceTemplate': {}, 38 | '/V1/ServiceTemplate?filter=eq,draft,False&limit=10&includeAttachments=False': {}, 39 | f'/V1/ServiceTemplate/{self.template_id}?forDeployment=true': {} 40 | } 41 | } 42 | 43 | def test_service_template_get(self): 44 | """ 45 | Test the get method of the ServiceTemplateClient. 46 | """ 47 | self.client.service_template.get() 48 | 49 | def test_service_template_get_with_filters(self): 50 | """ 51 | Test the get method of the ServiceTemplateClient with filters. 52 | """ 53 | self.client.service_template.get( 54 | filters=['eq,draft,False'], limit=10, include_attachments=False) 55 | 56 | def test_service_template_get_bad_status(self): 57 | """ 58 | Test the get method of the ServiceTemplateClient with a bad status. 59 | """ 60 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 61 | self.assertRaises(exceptions.PowerFlexClientException, 62 | self.client.service_template.get) 63 | 64 | def test_service_template_get_by_id(self): 65 | """ 66 | Test the get_by_id method of the ServiceTemplateClient. 67 | """ 68 | self.client.service_template.get_by_id(self.template_id, for_deployment=True) 69 | 70 | def test_service_template_get_by_id_bad_status(self): 71 | """ 72 | Test the get_by_id method of the ServiceTemplateClient with a bad status. 73 | """ 74 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 75 | self.assertRaises(exceptions.PowerFlexClientException, 76 | self.client.service_template.get_by_id, 77 | self.template_id, for_deployment=True) 78 | -------------------------------------------------------------------------------- /tests/test_snapshot_policy.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for testing snapshot policy client.""" 17 | 18 | # pylint: disable=invalid-name 19 | 20 | from PyPowerFlex import exceptions 21 | from PyPowerFlex.objects import snapshot_policy as sp 22 | import tests 23 | 24 | 25 | class TestSnapshotPolicyClient(tests.PyPowerFlexTestCase): 26 | """ 27 | Test class for snapshot policy client. 28 | """ 29 | def setUp(self): 30 | """ 31 | Set up the test case. 32 | """ 33 | super().setUp() 34 | self.client.initialize() 35 | self.fake_policy_id = '1' 36 | self.fake_volume_id = '1' 37 | 38 | self.MOCK_RESPONSES = { 39 | self.RESPONSE_MODE.Valid: { 40 | '/types/SnapshotPolicy/instances': 41 | {'id': self.fake_policy_id}, 42 | f'/instances/SnapshotPolicy::{self.fake_policy_id}': 43 | {'id': self.fake_policy_id}, 44 | f'/instances/SnapshotPolicy::{self.fake_policy_id}/action/removeSnapshotPolicy': 45 | {}, 46 | f'/instances/SnapshotPolicy::{self.fake_policy_id}' 47 | '/action/addSourceVolumeToSnapshotPolicy': 48 | {}, 49 | f'/instances/SnapshotPolicy::{self.fake_policy_id}/action/modifySnapshotPolicy': 50 | {}, 51 | f'/instances/SnapshotPolicy::{self.fake_policy_id}/action/pauseSnapshotPolicy': 52 | {}, 53 | f'/instances/SnapshotPolicy::{self.fake_policy_id}' 54 | '/action/removeSourceVolumeFromSnapshotPolicy': 55 | {}, 56 | f'/instances/SnapshotPolicy::{self.fake_policy_id}/action/renameSnapshotPolicy': 57 | {}, 58 | f'/instances/SnapshotPolicy::{self.fake_policy_id}/action/resumeSnapshotPolicy': 59 | {}, 60 | '/types/SnapshotPolicy' 61 | '/instances/action/querySelectedStatistics': { 62 | self.fake_policy_id: {'numOfSrcVols': 1} 63 | }, 64 | }, 65 | self.RESPONSE_MODE.Invalid: { 66 | '/types/SnapshotPolicy/instances': 67 | {}, 68 | } 69 | } 70 | 71 | def test_snapshot_policy_add_source_volume(self): 72 | """ 73 | Test adding a source volume to a snapshot policy. 74 | """ 75 | self.client.snapshot_policy.add_source_volume(self.fake_policy_id, 76 | self.fake_volume_id) 77 | 78 | def test_snapshot_policy_add_source_volume_bad_status(self): 79 | """ 80 | Test adding a source volume to a snapshot policy with a bad status. 81 | """ 82 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 83 | self.assertRaises(exceptions.PowerFlexClientException, 84 | self.client.snapshot_policy.add_source_volume, 85 | self.fake_policy_id, 86 | self.fake_volume_id) 87 | 88 | def test_snapshot_policy_create(self): 89 | """ 90 | Test creating a snapshot policy. 91 | """ 92 | self.client.snapshot_policy.create( 93 | auto_snap_creation_cadence_in_min=15, 94 | retained_snaps_per_level=[1, 2, 3], 95 | name='policy_1', 96 | paused=False 97 | ) 98 | 99 | def test_snapshot_policy_create_bad_status(self): 100 | """ 101 | Test creating a snapshot policy with a bad status. 102 | """ 103 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 104 | self.assertRaises(exceptions.PowerFlexFailCreating, 105 | self.client.snapshot_policy.create, 106 | auto_snap_creation_cadence_in_min=15, 107 | retained_snaps_per_level=[1, 2, 3], 108 | name='policy_1', 109 | paused=False) 110 | 111 | def test_snapshot_policy_create_no_id_in_response(self): 112 | """ 113 | Test creating a snapshot policy with no id in the response. 114 | """ 115 | with self.http_response_mode(self.RESPONSE_MODE.Invalid): 116 | self.assertRaises(KeyError, 117 | self.client.snapshot_policy.create, 118 | auto_snap_creation_cadence_in_min=15, 119 | retained_snaps_per_level=[1, 2, 3], 120 | name='policy_1', 121 | paused=False) 122 | 123 | def test_snapshot_policy_delete(self): 124 | """ 125 | Test deleting a snapshot policy. 126 | """ 127 | self.client.snapshot_policy.delete(self.fake_policy_id) 128 | 129 | def test_snapshot_policy_delete_bad_status(self): 130 | """ 131 | Test deleting a snapshot policy with a bad status. 132 | """ 133 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 134 | self.assertRaises(exceptions.PowerFlexFailDeleting, 135 | self.client.snapshot_policy.delete, 136 | self.fake_policy_id) 137 | 138 | def test_snapshot_policy_modify(self): 139 | """ 140 | Test modifying a snapshot policy. 141 | """ 142 | self.client.snapshot_policy.modify( 143 | self.fake_policy_id, 144 | auto_snap_creation_cadence_in_min=25, 145 | retained_snaps_per_level=[1, 2, 4] 146 | ) 147 | 148 | def test_snapshot_policy_modify_bad_status(self): 149 | """ 150 | Test modifying a snapshot policy with a bad status. 151 | """ 152 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 153 | self.assertRaises(exceptions.PowerFlexClientException, 154 | self.client.snapshot_policy.modify, 155 | self.fake_policy_id, 156 | auto_snap_creation_cadence_in_min=25, 157 | retained_snaps_per_level=[1, 2, 4]) 158 | 159 | def test_snapshot_policy_pause(self): 160 | """ 161 | Test pausing a snapshot policy. 162 | """ 163 | self.client.snapshot_policy.pause(self.fake_policy_id) 164 | 165 | def test_snapshot_policy_pause_bad_status(self): 166 | """ 167 | Test pausing a snapshot policy with a bad status. 168 | """ 169 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 170 | self.assertRaises(exceptions.PowerFlexClientException, 171 | self.client.snapshot_policy.pause, 172 | self.fake_policy_id) 173 | 174 | def test_snapshot_policy_remove_source_volume(self): 175 | """ 176 | Test removing a source volume from a snapshot policy. 177 | """ 178 | self.client.snapshot_policy.remove_source_volume( 179 | self.fake_policy_id, 180 | self.fake_volume_id, 181 | auto_snap_removal_action=sp.AutoSnapshotRemovalAction.detach, 182 | detach_locked_auto_snaps=True 183 | ) 184 | 185 | def test_snapshot_policy_remove_source_volume_bad_status(self): 186 | """ 187 | Test removing a source volume from a snapshot policy with a bad status. 188 | """ 189 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 190 | self.assertRaises(exceptions.PowerFlexClientException, 191 | self.client.snapshot_policy.remove_source_volume, 192 | self.fake_policy_id, 193 | self.fake_volume_id, 194 | sp.AutoSnapshotRemovalAction.remove, 195 | False) 196 | 197 | def test_snapshot_policy_rename(self): 198 | """ 199 | Test renaming a snapshot policy. 200 | """ 201 | self.client.snapshot_policy.rename(self.fake_policy_id, 202 | name='new_name') 203 | 204 | def test_snapshot_policy_rename_bad_status(self): 205 | """ 206 | Tests the behavior of the rename method when the HTTP response has a bad status. 207 | """ 208 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 209 | self.assertRaises(exceptions.PowerFlexFailRenaming, 210 | self.client.snapshot_policy.rename, 211 | self.fake_policy_id, 212 | name='new_name') 213 | 214 | def test_snapshot_policy_resume(self): 215 | """ 216 | Tests the behavior of the resume method. 217 | """ 218 | self.client.snapshot_policy.resume(self.fake_policy_id) 219 | 220 | def test_snapshot_policy_resume_bad_status(self): 221 | """ 222 | Tests the behavior of the resume method when the HTTP response has a bad status. 223 | """ 224 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 225 | self.assertRaises(exceptions.PowerFlexClientException, 226 | self.client.snapshot_policy.resume, 227 | self.fake_policy_id) 228 | 229 | def test_snapshot_policy_query_selected_statistics(self): 230 | """ 231 | Tests the behavior of the query_selected_statistics method. 232 | """ 233 | ret = self.client.snapshot_policy.query_selected_statistics( 234 | properties=["numOfSrcVols"] 235 | ) 236 | assert ret.get(self.fake_policy_id).get("numOfSrcVols") == 1 237 | 238 | def test_snapshot_policy_query_selected_statistics_bad_status(self): 239 | """ 240 | Tests the behavior of the query_selected_statistics method 241 | when the HTTP response has a bad status. 242 | """ 243 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 244 | self.assertRaises( 245 | exceptions.PowerFlexFailQuerying, 246 | self.client.snapshot_policy.query_selected_statistics, 247 | properties=["numOfSrcVols"], 248 | ) 249 | -------------------------------------------------------------------------------- /tests/test_utility.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc. or its subsidiaries. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Module for testing PowerFlex utility.""" 17 | 18 | # pylint: disable=invalid-name 19 | 20 | from PyPowerFlex import exceptions 21 | import tests 22 | 23 | 24 | class TestPowerFlexUtility(tests.PyPowerFlexTestCase): 25 | """ 26 | Test class for the PowerFlex utility. 27 | """ 28 | 29 | def setUp(self): 30 | """ 31 | Set up the test case. 32 | """ 33 | super().setUp() 34 | self.client.initialize() 35 | 36 | self.MOCK_RESPONSES = { 37 | self.RESPONSE_MODE.Valid: { 38 | '/types/StoragePool/instances/action/querySelectedStatistics': 39 | {}, 40 | '/types/Volume/instances/action/querySelectedStatistics': 41 | {}, 42 | } 43 | } 44 | 45 | def test_get_statistics_for_all_storagepools(self): 46 | """ 47 | Test the get_statistics_for_all_storagepools method. 48 | """ 49 | self.client.utility.get_statistics_for_all_storagepools() 50 | 51 | def test_get_statistics_for_all_storagepools_bad_status(self): 52 | """ 53 | Test the get_statistics_for_all_storagepools method with a bad status. 54 | """ 55 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 56 | self.assertRaises(exceptions.PowerFlexClientException, 57 | self.client.utility.get_statistics_for_all_storagepools) 58 | 59 | def test_get_statistics_for_all_volumes(self): 60 | """ 61 | Test the get_statistics_for_all_volumes method. 62 | """ 63 | self.client.utility.get_statistics_for_all_volumes() 64 | 65 | def test_get_statistics_for_all_volumes_bad_status(self): 66 | """ 67 | Test the get_statistics_for_all_volumes method with a bad status. 68 | """ 69 | with self.http_response_mode(self.RESPONSE_MODE.BadStatus): 70 | self.assertRaises(exceptions.PowerFlexClientException, 71 | self.client.utility.get_statistics_for_all_volumes) 72 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.14.0 3 | skip_missing_interpreters = true 4 | envlist = bandit,pep8,py{35,36,37,38},codecov 5 | ignore_basepython_conflict = true 6 | 7 | [testenv] 8 | basepython = python3 9 | install_command = pip install {opts} {packages} 10 | deps = -r {toxinidir}/test-requirements.txt 11 | whitelist_externals = find 12 | commands = find . -ignore_readdir_race -type f -name "*.pyc" -delete 13 | stestr run {posargs} --test-path ./tests 14 | 15 | [testenv:bandit] 16 | commands = bandit -r PyPowerFlex -n5 -ll 17 | 18 | [testenv:pep8] 19 | commands = flake8 {posargs} . 20 | 21 | [flake8] 22 | select = E,F,W,C 23 | ignore = W503,W504 24 | application-import-names = PyPowerFlex,tests 25 | import-order-style = google 26 | copyright-check = True 27 | copyright-author = Dell Inc. 28 | 29 | [testenv:codecov] 30 | commands = coverage erase 31 | coverage run 32 | coverage report 33 | --------------------------------------------------------------------------------