diff --git a/ChangeLog.md b/ChangeLog.md index 787bfe4..061e994 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,6 +1,9 @@ # PyPowerFlex Change Log -## Version 1.6.0 - released on 20/12/22 +## Version 1.7.0 - released on 31/03/23 +- Added block provisioning operations includes getting details, adding, pause, resume and removing a replication pair. + +## Version 1.6.0 - released on 28/12/22 - Added block provisioning operations includes getting details, creating, modifying, creating snapshots, pause, resume, freeze, unfreeze, activate, inactivate and deleting a replication consistency group. diff --git a/PyPowerFlex/__init__.py b/PyPowerFlex/__init__.py index 53e3e2a..f54dfd1 100644 --- a/PyPowerFlex/__init__.py +++ b/PyPowerFlex/__init__.py @@ -43,7 +43,8 @@ class PowerFlexClient: 'system', 'volume', 'utility', - 'replication_consistency_group' + 'replication_consistency_group', + 'replication_pair' ) def __init__(self, @@ -90,6 +91,7 @@ def initialize(self): self.__add_storage_entity('volume', objects.Volume) self.__add_storage_entity('utility', objects.PowerFlexUtility) self.__add_storage_entity('replication_consistency_group', objects.ReplicationConsistencyGroup) + self.__add_storage_entity('replication_pair', objects.ReplicationPair) utils.init_logger(self.configuration.log_level) if version.parse(self.system.api_version()) < version.Version('3.0'): raise exceptions.PowerFlexClientException( diff --git a/PyPowerFlex/objects/__init__.py b/PyPowerFlex/objects/__init__.py index 8599d54..23cb063 100644 --- a/PyPowerFlex/objects/__init__.py +++ b/PyPowerFlex/objects/__init__.py @@ -25,6 +25,7 @@ from PyPowerFlex.objects.volume import Volume from PyPowerFlex.objects.utility import PowerFlexUtility from PyPowerFlex.objects.replication_consistency_group import ReplicationConsistencyGroup +from PyPowerFlex.objects.replication_pair import ReplicationPair __all__ = [ @@ -39,5 +40,6 @@ 'System', 'Volume', 'PowerFlexUtility', - 'ReplicationConsistencyGroup' + 'ReplicationConsistencyGroup', + 'ReplicationPair' ] diff --git a/PyPowerFlex/objects/replication_consistency_group.py b/PyPowerFlex/objects/replication_consistency_group.py index 7ee4b21..0efb523 100644 --- a/PyPowerFlex/objects/replication_consistency_group.py +++ b/PyPowerFlex/objects/replication_consistency_group.py @@ -235,6 +235,16 @@ def rename_rcg(self, rcg_id, new_name): return self._perform_entity_operation_based_on_action\ (rcg_id, "rename", params=params) + def get_replication_pairs(self, rcg_id): + """Get replication pairs of PowerFlex RCG. + + :param rcg_id: str + :return: dict + """ + + return self.get_related(rcg_id, + 'ReplicationPair') + def get_all_statistics(self, api_version_less_than_3_6): """list statistics of all replication consistency groups for PowerFlex. :param api_version_less_than_3_6: bool diff --git a/PyPowerFlex/objects/replication_pair.py b/PyPowerFlex/objects/replication_pair.py new file mode 100644 index 0000000..5172933 --- /dev/null +++ b/PyPowerFlex/objects/replication_pair.py @@ -0,0 +1,103 @@ +# Copyright (c) 2023 Dell Inc. or its subsidiaries. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +import requests + +from PyPowerFlex import base_client +from PyPowerFlex import exceptions + + +LOG = logging.getLogger(__name__) + + +class ReplicationPair(base_client.EntityRequest): + def get_statistics(self, id): + """Retrieve statistics for the specified ReplicationPair object. + + :type id: str + :rtype: dict + """ + + return self.get_related(id, + 'Statistics') + + def add(self, + source_vol_id, + dest_vol_id, + rcg_id, + copy_type, + name=None): + """Add replication pair to PowerFlex RCG. + + :param source_vol_id: str + :param dest_vol_id: str + :param rcg_id: str + :param copy_type: str + :type name: str + :return: dict + """ + + params = dict( + sourceVolumeId=source_vol_id, + destinationVolumeId=dest_vol_id, + replicationConsistencyGroupId=rcg_id, + copyType=copy_type, + name=name + ) + + return self._create_entity(params) + + def remove(self, id): + """Remove replication pair of PowerFlex RCG. + + :param id: str + :return: None + """ + return self._delete_entity(id) + + def pause(self, id): + """Pause the progress of the specified ReplicationPair's initial copy. + + :param id: str + :return: dict + """ + return self._perform_entity_operation_based_on_action\ + (id, "pausePairInitialCopy", add_entity=False) + + def resume(self, id): + """Resume initial copy of the ReplicationPair. + + :param id: str + :return: dict + """ + return self._perform_entity_operation_based_on_action\ + (id, "resumePairInitialCopy", add_entity=False) + + def get_all_statistics(self): + """Retrieve statistics for all ReplicationPair objects. + :return: dict + """ + r, response = self.send_post_request(self.list_statistics_url, + entity=self.entity, + action="querySelectedStatistics") + if r.status_code != requests.codes.ok: + msg = ('Failed to list statistics for all ReplicationPair objects. ' + 'Error: {response}'.format(response=response)) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response diff --git a/README.md b/README.md index 76fde71..066c118 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ python setup.py install * Sds * SnapshotPolicy * ReplicationConsistencyGroup +* ReplicationPair * System * StoragePool * AccelerationPool diff --git a/setup.py b/setup.py index 48fc502..f5baf3d 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ setup( name='PyPowerFlex', - version='1.6.0', + version='1.7.0', description='Python library for Dell PowerFlex', author='Ansible Team at Dell', author_email='ansible.team@dell.com', diff --git a/tests/test_replication_consistency_group.py b/tests/test_replication_consistency_group.py index 3108a5f..6ab38b0 100644 --- a/tests/test_replication_consistency_group.py +++ b/tests/test_replication_consistency_group.py @@ -76,6 +76,9 @@ def setUp(self): {'id': self.fake_rcg_id}, '/types/ReplicationConsistencyGroup/instances/action/querySelectedStatistics': {}, + '/instances/ReplicationConsistencyGroup::{}' + '/relationships/ReplicationPair'.format(self.fake_rcg_id): + {'id': self.fake_rcg_id}, }, self.RESPONSE_MODE.Invalid: { '/types/ReplicationConsistencyGroup/instances': @@ -132,6 +135,9 @@ def test_modify_target_volume_access_mode(self): def test_rename_rcg(self): self.client.replication_consistency_group.rename_rcg(self.fake_rcg_id, new_name="rename") + def test_get_replication_pairs(self): + self.client.replication_consistency_group.get_replication_pairs(self.fake_rcg_id) + def test_get_all_statistics(self): self.client.replication_consistency_group.get_all_statistics(True) diff --git a/tests/test_replication_pair.py b/tests/test_replication_pair.py new file mode 100644 index 0000000..660609f --- /dev/null +++ b/tests/test_replication_pair.py @@ -0,0 +1,84 @@ +# Copyright (c) 2023 Dell Inc. or its subsidiaries. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from PyPowerFlex import exceptions +from PyPowerFlex.objects import replication_pair +import tests + + +class TestReplicationPairClient(tests.PyPowerFlexTestCase): + def setUp(self): + super(TestReplicationPairClient, self).setUp() + self.client.initialize() + self.fake_replication_pair_id = '1' + + self.MOCK_RESPONSES = { + self.RESPONSE_MODE.Valid: { + '/types/ReplicationPair/instances': + {'id': self.fake_replication_pair_id}, + '/instances/ReplicationPair::{}'.format(self.fake_replication_pair_id): + {'id': self.fake_replication_pair_id}, + '/instances/ReplicationPair::{}' + '/action/removeReplicationPair'.format(self.fake_replication_pair_id): + {}, + '/instances/ReplicationPair::{}' + '/action/pausePairInitialCopy'.format(self.fake_replication_pair_id): + {'id': self.fake_replication_pair_id}, + '/instances/ReplicationPair::{}' + '/action/resumePairInitialCopy'.format(self.fake_replication_pair_id): + {'id': self.fake_replication_pair_id}, + '/types/ReplicationPair/instances/action/querySelectedStatistics': + {}, + }, + self.RESPONSE_MODE.Invalid: { + '/types/ReplicationPair/instances': + {}, + } + } + + def test_add_replication_pair(self): + self.client.replication_pair.add\ + (source_vol_id='1', dest_vol_id='1', + rcg_id='1', copy_type='OnlineCopy', name='test') + + def test_remove_replication_pair(self): + self.client.replication_pair.remove(self.fake_replication_pair_id) + + def test_pause_online_copy(self): + self.client.replication_pair.pause(self.fake_replication_pair_id) + + def test_resume_online_copy(self): + self.client.replication_pair.resume(self.fake_replication_pair_id) + + def test_get_all_statistics(self): + self.client.replication_pair.get_all_statistics() + + def test_add_replication_pair_bad_status(self): + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexFailCreating, + self.client.replication_pair.add, + source_vol_id='1', dest_vol_id='1', + rcg_id='1', copy_type='OnlineCopy', name='test') + + def test_remove_replication_pair_bad_status(self): + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexFailDeleting, + self.client.replication_pair.remove, + self.fake_replication_pair_id) + + def test_get_all_statistics_bad_status(self): + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.replication_pair.get_all_statistics)