From f95c4175ab4e7352aa9f5c6f11a60558fd569d90 Mon Sep 17 00:00:00 2001 From: Laszlo Szomor Date: Mon, 10 Apr 2023 21:24:32 +0200 Subject: [PATCH] [FEATURE] Add refresh support for snapshot, filesystem_snapshot, volume, consistencygroup "refreshed" state added to: * snapshot * filesystem_snapshot * volume * consistencygroup --- plugins/doc_fragments/unity.py | 2 +- plugins/modules/consistencygroup.py | 123 +++++++++++++++++- plugins/modules/filesystem_snapshot.py | 77 ++++++++++- plugins/modules/snapshot.py | 79 ++++++++++- plugins/modules/volume.py | 122 ++++++++++++++++- requirements.txt | 2 +- .../module_utils/mock_consistencygroup_api.py | 48 +++++++ .../plugins/module_utils/mock_volume_api.py | 62 +++++++++ .../plugins/modules/test_consistencygroup.py | 34 +++++ tests/unit/plugins/modules/test_volume.py | 32 +++++ 10 files changed, 567 insertions(+), 14 deletions(-) diff --git a/plugins/doc_fragments/unity.py b/plugins/doc_fragments/unity.py index 1ebc7f4..181106f 100644 --- a/plugins/doc_fragments/unity.py +++ b/plugins/doc_fragments/unity.py @@ -46,7 +46,7 @@ class ModuleDocFragment(object): - A Dell Unity Storage device version 5.1 or later. - Ansible-core 2.12 or later. - Python 3.9, 3.10 or 3.11. - - Storops Python SDK 1.2.11. + - Storops Python SDK > 1.2.11. notes: - The modules present in this collection named as 'dellemc.unity' are built to support the Dell Unity storage platform. diff --git a/plugins/modules/consistencygroup.py b/plugins/modules/consistencygroup.py index 14e4de5..cf2b3d8 100644 --- a/plugins/modules/consistencygroup.py +++ b/plugins/modules/consistencygroup.py @@ -20,7 +20,8 @@ consistency group, unmapping hosts from consistency group, renaming consistency group, modifying attributes of consistency group, enabling replication in consistency group, disabling replication in - consistency group and deleting consistency group. + consistency group, deleting consistency group + and refreshing thin clone consistency group. extends_documentation_fragment: - dellemc.unity.unity @@ -197,9 +198,34 @@ state: description: - Define whether the consistency group should exist or not. - choices: [absent, present] + - The C(refreshed) state is not idempotent. It always executes the + refresh operation. + choices: [absent, present, refreshed] required: true type: str + retention_duration: + description: + - This option is for specifying the retention duration for the backup copy + of the snapshot created during the refresh operation. + - The retention duration is set in seconds. See the examples how to + calculate it. + - If set to C(0), the backup copy is deleted immediately + - If not set, the storage defaults are used + type: int + copy_name: + description: + - The backup copy name of the snapshot created by the refresh operation. + type: str + force_refresh: + description: + - When set to C(true), the refresh operation will proceed even if host + access is configured on the storage resource. + type: bool + default: false + snapshot_name: + description: + - The source snapshot which is used to refresh the thin clone + type: str notes: - The I(check_mode) is not supported. """ @@ -337,6 +363,48 @@ cg_name: "dis_repl_ans_source" replication_state: "disable" state: "present" + +- name: Force Refresh a thin clone, keep backup snapshot for 2 days + dellemc.unity.consistencygroup: + unispherehost: "{{unispherehost}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + cg_name: "{{cg_name}}" + retention_duration: "{{ '2days' | community.general.to_seconds | int }}" + force_refresh: True + state: "refreshed" + +- name: Refresh a Snapshot, delete backup snapshot + dellemc.unity.consistencygroup: + unispherehost: "{{unispherehost}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + cg_name: "{{cg_name}}" + retention_duration: 0 + state: "refreshed" + +- name: Refresh a thin clone and set backup snapshot name + dellemc.unity.consistencygroup: + unispherehost: "{{unispherehost}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + cg_name: "{{cg_name}}" + copy_name: "{{snapshot_name}}_before_refresh" + state: "refreshed" + +- name: Refresh a thin clone from a snapshot, keep backup snapshot for 2 days + dellemc.unity.consistencygroup: + unispherehost: "{{unispherehost}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + cg_name: "{{cg_name}}" + snapshot_name: "{{new_snapshot_name}}" + retention_duration: "{{ '2days' | community.general.to_seconds | int }}" + state: "refreshed" """ RETURN = r''' @@ -1062,6 +1130,35 @@ def delete_cg(self, cg_name): LOG.error(errormsg) self.module.fail_json(msg=errormsg) + def refresh_cg(self, cg_name, snapshot=None, copy_name=None, force=False, + retention_duration=None): + """Refresh thin clone + + :param copy_name: name of the backup snapshot + :param force: proceeed refresh even if host access is configured + :param retention_duration: Backup copy retention duration in seconds + """ + cg_obj = self.return_cg_instance(cg_name) + try: + if snapshot is not None: + resp = snapshot\ + .refresh_thin_clone(sr=cg_obj, + copy_name=copy_name, + force=force, + retention_duration=retention_duration) + else: + resp = cg_obj.refresh(copy_name=copy_name, + force=force, + retention_duration=retention_duration) + resp.raise_if_err() + cg_obj.update() + except Exception as e: + error_msg = "Failed to refresh thin clone" \ + " [name: %s, id: %s] with error %s"\ + % (cg_obj.name, cg_obj.id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + def refine_volumes(self, volumes): """Refine volumes. :param volumes: Volumes that is to be added/removed @@ -1286,6 +1383,10 @@ def perform_module_operation(self): mapping_state = self.module.params['mapping_state'] replication = self.module.params['replication_params'] replication_state = self.module.params['replication_state'] + retention_duration = self.module.params['retention_duration'] + copy_name = self.module.params['copy_name'] + force_refresh = self.module.params['force_refresh'] + snapshot_name = self.module.params['snapshot_name'] state = self.module.params['state'] # result is a dictionary that contains changed status and consistency @@ -1399,6 +1500,16 @@ def perform_module_operation(self): else: result['changed'] = self.disable_cg_replication(cg_name) + if state == 'refreshed' and cg_details: + snapshot = None + if snapshot_name is not None: + snapshot = self.unity_conn.get_snap(name=snapshot_name) + + self.refresh_cg(cg_name=cg_name, snapshot=snapshot, + copy_name=copy_name, force=force_refresh, + retention_duration=retention_duration) + result['changed'] = True + if result['create_cg'] or result['modify_cg'] or result[ 'add_vols_to_cg'] or result['remove_vols_from_cg'] or result[ 'delete_cg'] or result['rename_cg'] or result[ @@ -1501,7 +1612,13 @@ def get_consistencygroup_parameters(): destination_pool_id=dict(type='str') )), replication_state=dict(type='str', choices=['enable', 'disable']), - state=dict(required=True, type='str', choices=['present', 'absent']) + retention_duration=dict(required=False, type='int'), + copy_name=dict(required=False, type='str'), + force_refresh=dict(required=False, type='bool', + default=False), + snapshot_name=dict(required=False, type='str'), + state=dict(required=True, type='str', + choices=['present', 'absent', 'refreshed']) ) diff --git a/plugins/modules/filesystem_snapshot.py b/plugins/modules/filesystem_snapshot.py index 35e536a..55a364b 100644 --- a/plugins/modules/filesystem_snapshot.py +++ b/plugins/modules/filesystem_snapshot.py @@ -16,7 +16,7 @@ description: - Managing Filesystem Snapshot on the Unity storage system includes create filesystem snapshot, get filesystem snapshot, modify filesystem - snapshot and delete filesystem snapshot. + snapshot, delete filesystem snapshot and refresh filesystem snapshot. version_added: '1.1.0' extends_documentation_fragment: - dellemc.unity.unity @@ -99,13 +99,26 @@ - If not given, snapshot's access type will be C(Checkpoint). type: str choices: ['Checkpoint' , 'Protocol'] + retention_duration: + description: + - This option is for specifying the retention duration for the backup copy + of the snapshot created during the refresh operation. + - The retention duration is set in seconds. See the examples how to + calculate it. + - If set to C(0), the backup copy is deleted immediately + - If not set, the storage defaults are used + type: int + copy_name: + description: + - The backup copy name of the snapshot created by the refresh operation. + type: str state: description: - The state option is used to mention the existence of the filesystem snapshot. type: str required: true - choices: ['absent', 'present'] + choices: ['absent', 'present', 'refreshed'] notes: - Filesystem snapshot cannot be deleted, if it has nfs or smb share. - The I(check_mode) is not supported. @@ -197,6 +210,37 @@ validate_certs: "{{validate_certs}}" snapshot_id: "10008000403" state: "absent" + + - name: Refresh a Snapshot, keep backup snapshot for 3 days + dellemc.unity.snapshot: + unispherehost: "{{unispherehost}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + snapshot_name: "ansible_test_FS_snap" + retention_duration: "{{ '3days' | community.general.to_seconds | int }}" + state: "refreshed" + + - name: Refresh a Snapshot, delete backup snapshot + dellemc.unity.snapshot: + unispherehost: "{{unispherehost}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + snapshot_name: "ansible_test_FS_snap" + retention_duration: 0 + state: "refreshed" + + - name: Refresh a Snapshot and set backup snapshot name + dellemc.unity.snapshot: + unispherehost: "{{unispherehost}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + snapshot_name: "ansible_test_FS_snap" + copy_name: "{{snapshot_name}}_before_refresh" + state: "refreshed" + ''' RETURN = r''' @@ -471,6 +515,20 @@ def delete_fs_snapshot(self, fs_snapshot): LOG.error(error_msg) self.module.fail_json(msg=error_msg) + def refresh_fs_snapshot(self, fs_snapshot, copy_name=None, + retention_duration=None): + try: + resp = fs_snapshot.refresh(copy_name=copy_name, + retention_duration=retention_duration) + resp.raise_if_err() + fs_snapshot.update() + except Exception as e: + error_msg = "Failed to refresh snapshot" \ + " [name: %s , id: %s] with error %s"\ + % (fs_snapshot.name, fs_snapshot.id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + def get_fs_snapshot_obj(self, name=None, id=None): fs_snapshot = id if id else name msg = "Failed to get details of filesystem snapshot %s with error %s." @@ -585,6 +643,8 @@ def perform_module_operation(self): description = self.module.params['description'] fs_access_type = self.module.params['fs_access_type'] state = self.module.params['state'] + retention_duration = self.module.params['retention_duration'] + copy_name = self.module.params['copy_name'] nas_server_resource = None filesystem_resource = None changed = False @@ -707,6 +767,14 @@ def perform_module_operation(self): fs_snapshot = self.delete_fs_snapshot(fs_snapshot) changed = True + # Refresh the snapshot: + if fs_snapshot and state == "refreshed": + fs_snapshot = self\ + .refresh_fs_snapshot(fs_snapshot=fs_snapshot, + copy_name=copy_name, + retention_duration=retention_duration) + changed = True + # Add filesystem snapshot details to the result. if fs_snapshot: fs_snapshot.update() @@ -754,7 +822,10 @@ def get_snapshot_parameters(): description=dict(required=False, type='str'), fs_access_type=dict(required=False, type='str', choices=['Checkpoint', 'Protocol']), - state=dict(required=True, type='str', choices=['present', 'absent']) + retention_duration=dict(required=False, type='int'), + copy_name=dict(required=False, type='str'), + state=dict(required=True, type='str', + choices=['present', 'absent', 'refreshed']) ) diff --git a/plugins/modules/snapshot.py b/plugins/modules/snapshot.py index c8aba18..bda9479 100644 --- a/plugins/modules/snapshot.py +++ b/plugins/modules/snapshot.py @@ -15,7 +15,8 @@ short_description: Manage snapshots on the Unity storage system description: - Managing snapshots on the Unity storage system includes create snapshot, - delete snapshot, update snapshot, get snapshot, map host and unmap host. + delete snapshot, update snapshot, get snapshot, refresh snapshot, + map host and unmap host. version_added: '1.1.0' extends_documentation_fragment: @@ -82,9 +83,11 @@ description: - The I(state) option is used to mention the existence of the snapshot. + - The C(refreshed) state is not idempotent. It always executes the + refresh operation. type: str required: true - choices: [ 'absent', 'present' ] + choices: [ 'absent', 'present', 'refreshed' ] host_name: description: - The name of the host. @@ -106,6 +109,19 @@ - It is required when a snapshot is mapped or unmapped from host. type: str choices: ['mapped', 'unmapped'] + retention_duration: + description: + - This option is for specifying the retention duration for the backup copy + of the snapshot created during the refresh operation. + - The retention duration is set in seconds. See the examples how to + calculate it. + - If set to C(0), the backup copy is deleted immediately + - If not set, the storage defaults are used + type: int + copy_name: + description: + - The backup copy name of the snapshot created by the refresh operation. + type: str notes: - The I(check_mode) is not supported. @@ -185,6 +201,36 @@ validate_certs: "{{validate_certs}}" snapshot_name: "{{cg_snapshot_name}}" state: "absent" + + - name: Refresh a Snapshot, keep backup snapshot for 2 days + dellemc.unity.snapshot: + unispherehost: "{{unispherehost}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + snapshot_name: "{{snapshot_name}}" + retention_duration: "{{ '2days' | community.general.to_seconds | int }}" + state: "refreshed" + + - name: Refresh a Snapshot, delete backup snapshot + dellemc.unity.snapshot: + unispherehost: "{{unispherehost}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + snapshot_name: "{{snapshot_name}}" + retention_duration: 0 + state: "refreshed" + + - name: Refresh a Snapshot and set backup snapshot name + dellemc.unity.snapshot: + unispherehost: "{{unispherehost}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + snapshot_name: "{{snapshot_name}}" + copy_name: "{{snapshot_name}}_before_refresh" + state: "refreshed" ''' RETURN = r''' @@ -394,6 +440,20 @@ def delete_snapshot(self, snapshot): LOG.error(error_msg) self.module.fail_json(msg=error_msg) + def refresh_snapshot(self, snapshot, copy_name=None, + retention_duration=None): + try: + resp = snapshot.refresh(copy_name=copy_name, + retention_duration=retention_duration) + resp.raise_if_err() + snapshot.update() + except Exception as e: + error_msg = "Failed to refresh snapshot" \ + " [name: %s , id: %s] with error %s"\ + % (snapshot.name, snapshot.id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + def get_snapshot_obj(self, name=None, id=None): snapshot = id if id else name msg = "Failed to get details of snapshot %s with error %s " @@ -480,6 +540,8 @@ def perform_module_operation(self): host_id = self.module.params['host_id'] host_state = self.module.params['host_state'] state = self.module.params['state'] + retention_duration = self.module.params['retention_duration'] + copy_name = self.module.params['copy_name'] host = None storage_resource = None changed = False @@ -615,6 +677,14 @@ def perform_module_operation(self): snapshot = self.delete_snapshot(snapshot) changed = True + # Refresh the snapshot: + if snapshot and state == "refreshed": + snapshot = self\ + .refresh_snapshot(snapshot=snapshot, + copy_name=copy_name, + retention_duration=retention_duration) + changed = True + # Add snapshot details to the result. if snapshot: snapshot.update() @@ -736,7 +806,10 @@ def get_snapshot_parameters(): host_id=dict(required=False, type='str'), host_state=dict(required=False, type='str', choices=['mapped', 'unmapped']), - state=dict(required=True, type='str', choices=['present', 'absent']) + retention_duration=dict(required=False, type='int'), + copy_name=dict(required=False, type='str'), + state=dict(required=True, type='str', + choices=['present', 'absent', 'refreshed']) ) diff --git a/plugins/modules/volume.py b/plugins/modules/volume.py index 82bcb01..152f9b3 100644 --- a/plugins/modules/volume.py +++ b/plugins/modules/volume.py @@ -21,7 +21,8 @@ Map Volume to host, Unmap volume to host, Display volume details, - Delete volume. + Delete volume, + Refresh thin clone volume. extends_documentation_fragment: - dellemc.unity.unity @@ -135,7 +136,9 @@ state: description: - State variable to determine whether volume will exist or not. - choices: ['absent', 'present'] + - The C(refreshed) state is not idempotent. It always executes the + refresh operation. + choices: ['absent', 'present', 'refreshed'] required: true type: str hosts: @@ -160,6 +163,29 @@ - If I(hlu) is not specified, unity will choose it automatically. The maximum value supported is C(255). type: str + retention_duration: + description: + - This option is for specifying the retention duration for the backup copy + of the snapshot created during the refresh operation. + - The retention duration is set in seconds. See the examples how to + calculate it. + - If set to C(0), the backup copy is deleted immediately + - If not set, the storage defaults are used + type: int + copy_name: + description: + - The backup copy name of the snapshot created by the refresh operation. + type: str + force_refresh: + description: + - When set to C(true), the refresh operation will proceed even if host + access is configured on the storage resource. + type: bool + default: false + snapshot_name: + description: + - The source snapshot which is used to refresh the thin clone + type: str notes: - The I(check_mode) is not supported. @@ -262,6 +288,48 @@ validate_certs: "{{validate_certs}}" vol_id: "{{vol_id}}" state: "{{state_absent}}" + +- name: Force Refresh a thin clone, keep backup snapshot for 2 days + dellemc.unity.volume: + unispherehost: "{{unispherehost}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + vol_name: "{{vol_name}}" + retention_duration: "{{ '2days' | community.general.to_seconds | int }}" + force_refresh: True + state: "refreshed" + +- name: Refresh a Snapshot, delete backup snapshot + dellemc.unity.snapshot: + unispherehost: "{{unispherehost}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + vol_name: "{{vol_name}}" + retention_duration: 0 + state: "refreshed" + +- name: Refresh a thin clone and set backup snapshot name + dellemc.unity.snapshot: + unispherehost: "{{unispherehost}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + vol_name: "{{vol_name}}" + copy_name: "{{snapshot_name}}_before_refresh" + state: "refreshed" + +- name: Refresh a thin clone from a snapshot, keep backup snapshot for 2 days + dellemc.unity.volume: + unispherehost: "{{unispherehost}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + vol_name: "{{vol_name}}" + snapshot_name: "{{new_snapshot_name}}" + retention_duration: "{{ '2days' | community.general.to_seconds | int }}" + state: "refreshed" """ RETURN = r''' @@ -934,6 +1002,34 @@ def delete_volume(self, vol_id): LOG.error(errormsg) self.module.fail_json(msg=errormsg) + def refresh_volume(self, obj_vol, snapshot=None, copy_name=None, + force=False, retention_duration=None): + """Refresh thin clone + + :param copy_name: name of the backup snapshot + :param force: proceeed refresh even if host access is configured + :param retention_duration: Backup copy retention duration in seconds + """ + try: + if snapshot is not None: + resp = snapshot\ + .refresh_thin_clone(sr=obj_vol.storage_resource, + copy_name=copy_name, + force=force, + retention_duration=retention_duration) + else: + resp = obj_vol.refresh(copy_name=copy_name, + force=force, + retention_duration=retention_duration) + resp.raise_if_err() + obj_vol.update() + except Exception as e: + error_msg = "Failed to refresh thin clone" \ + " [name: %s, id: %s] with error %s"\ + % (obj_vol.name, obj_vol.id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + def get_volume_host_access_list(self, obj_vol): """ Get volume host access list @@ -1078,6 +1174,10 @@ def perform_module_operation(self): hlu = self.module.params['hlu'] mapping_state = self.module.params['mapping_state'] new_vol_name = self.module.params['new_vol_name'] + retention_duration = self.module.params['retention_duration'] + copy_name = self.module.params['copy_name'] + force_refresh = self.module.params['force_refresh'] + snapshot_name = self.module.params['snapshot_name'] state = self.module.params['state'] hosts = self.module.params['hosts'] @@ -1226,6 +1326,16 @@ def perform_module_operation(self): volume_details = self.get_volume_display_attributes( obj_vol=obj_vol) + if state == 'refreshed' and volume_details: + snapshot = None + if snapshot_name is not None: + snapshot = self.unity_conn.get_snap(name=snapshot_name) + + self.refresh_volume(obj_vol=obj_vol, snapshot=snapshot, + copy_name=copy_name, force=force_refresh, + retention_duration=retention_duration) + changed = True + result['changed'] = changed result['volume_details'] = volume_details self.module.exit_json(**result) @@ -1262,7 +1372,13 @@ def get_volume_parameters(): new_vol_name=dict(required=False, type='str'), tiering_policy=dict(required=False, type='str', choices=[ 'AUTOTIER_HIGH', 'AUTOTIER', 'HIGHEST', 'LOWEST']), - state=dict(required=True, type='str', choices=['present', 'absent']) + retention_duration=dict(required=False, type='int'), + copy_name=dict(required=False, type='str'), + force_refresh=dict(required=False, type='bool', + default=False), + snapshot_name=dict(required=False, type='str'), + state=dict(required=True, type='str', + choices=['present', 'absent', 'refreshed']) ) diff --git a/requirements.txt b/requirements.txt index 2325e97..fd8cc01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ urllib3 -storops>=1.2.11 +storops>1.2.11 setuptools diff --git a/tests/unit/plugins/module_utils/mock_consistencygroup_api.py b/tests/unit/plugins/module_utils/mock_consistencygroup_api.py index 07fe6b5..ecc96b6 100644 --- a/tests/unit/plugins/module_utils/mock_consistencygroup_api.py +++ b/tests/unit/plugins/module_utils/mock_consistencygroup_api.py @@ -30,6 +30,10 @@ class MockConsistenyGroupApi: 'mapping_state': None, 'replication_params': {}, 'replication_state': None, + 'retention_duration': None, + 'copy_name': None, + 'force_refresh': False, + 'snapshot_name': None, 'state': None } IP_ADDRESS_MOCK_VALUE = '***.***.***.**' @@ -69,6 +73,41 @@ def get_cg_object(): 'type': 'StorageResourceTypeEnum.CONSISTENCY_GROUP', 'virtual_volumes': None, 'vmware_uuid': None, 'existed': True, 'snapshots': [], 'cg_replication_enabled': False}) + @staticmethod + def cg_get_refreshable_details_method_response(): + return {'advanced_dedup_status': 'DedupStatusEnum.DISABLED', 'block_host_access': None, 'data_reduction_percent': 0, + 'data_reduction_ratio': 1.0, 'data_reduction_size_saved': 0, 'data_reduction_status': 'DataReductionStatusEnum.DISABLED', + 'datastores': None, 'dedup_status': None, 'description': '', 'esx_filesystem_block_size': None, + 'esx_filesystem_major_version': None, 'filesystem': None, 'health': {}, 'host_v_vol_datastore': None, + 'id': 'cg_id_2', 'is_replication_destination': False, 'is_snap_schedule_paused': None, + 'luns': [{'id': 'lun_id_2', 'name': 'test_lun_cg_issue_clone', 'is_thin_enabled': False, + 'size_total': 1, 'is_data_reduction_enabled': False}], + 'name': 'lun_test_cg_clone', 'per_tier_size_used': [1, 0, 0], + 'pools': [{'id': 'pool_id_1'}], + 'relocation_policy': 'TieringPolicyEnum.AUTOTIER_HIGH', 'replication_type': 'ReplicationTypeEnum.NONE', + 'size_allocated': 0, 'size_total': 1, 'size_used': None, 'snap_count': 0, 'snap_schedule': None, + 'snaps_size_allocated': 0, 'snaps_size_total': 0, 'thin_status': 'ThinStatusEnum.TRUE', + 'type': 'StorageResourceTypeEnum.CONSISTENCY_GROUP', 'virtual_volumes': None, 'vmware_uuid': None, + 'existed': True, 'snapshots': [], 'cg_replication_enabled': False, 'is_thin_clone': True} + + @staticmethod + def get_refreshable_cg_object(): + return MockSDKObject({'advanced_dedup_status': 'DedupStatusEnum.DISABLED', 'block_host_access': None, + 'data_reduction_percent': 0, 'data_reduction_ratio': 1.0, 'data_reduction_size_saved': 0, + 'data_reduction_status': 'DataReductionStatusEnum.DISABLED', + 'datastores': None, 'dedup_status': None, 'description': '', 'esx_filesystem_block_size': None, + 'esx_filesystem_major_version': None, 'filesystem': None, 'health': {}, 'host_v_vol_datastore': None, + 'id': 'cg_id_2', 'is_replication_destination': False, 'is_snap_schedule_paused': None, + 'luns': [MockSDKObject({'id': 'lun_id_2', 'name': 'test_lun_cg_issue_clone', + 'is_thin_enabled': False, 'size_total': 1, 'is_data_reduction_enabled': False})], + 'name': 'lun_test_cg_clone', 'per_tier_size_used': [1, 0, 0], + 'pools': [MockSDKObject({'id': 'pool_id_1'})], + 'relocation_policy': 'TieringPolicyEnum.AUTOTIER_HIGH', 'replication_type': 'ReplicationTypeEnum.NONE', + 'size_allocated': 0, 'size_total': 1, 'size_used': None, 'snap_count': 0, 'snap_schedule': None, + 'snaps_size_allocated': 0, 'snaps_size_total': 0, 'thin_status': 'ThinStatusEnum.TRUE', + 'type': 'StorageResourceTypeEnum.CONSISTENCY_GROUP', 'virtual_volumes': None, 'vmware_uuid': None, + 'existed': True, 'snapshots': [], 'cg_replication_enabled': False, 'is_thin_clone': True}) + @staticmethod def get_cg_replication_dependent_response(response_type): if response_type == 'cg_replication_enabled_details': @@ -120,3 +159,12 @@ def get_remote_system_conn_response(): conn = MockConsistenyGroupApi.get_cg_replication_dependent_response("remote_system")[0] conn.get_pool = MagicMock(return_value=MockConsistenyGroupApi.get_cg_replication_dependent_response('remote_system_pool_object')) return conn + + @staticmethod + def refresh_cg_response(response_type): + if response_type == 'api': + return {'copy': { + 'id': "85899345930" + }} + else: + return 'Failed to refresh thin clone [name: lun_test_cg_clone, id: cg_id_2] with error' diff --git a/tests/unit/plugins/module_utils/mock_volume_api.py b/tests/unit/plugins/module_utils/mock_volume_api.py index 82097a3..45fcb7d 100644 --- a/tests/unit/plugins/module_utils/mock_volume_api.py +++ b/tests/unit/plugins/module_utils/mock_volume_api.py @@ -36,6 +36,10 @@ class MockVolumeApi: 'mapping_state': None, 'new_vol_name': None, 'tiering_policy': None, + 'retention_duration': None, + 'copy_name': None, + 'force_refresh': False, + 'snapshot_name': None, 'state': None, } @@ -172,3 +176,61 @@ def modify_volume_response(response_type): }} else: return 'Failed to modify the volume Atest with error' + + @staticmethod + def refreshable_volume_response(response_type): + if response_type == 'api': + return {'volume_details': { + 'current_node': 'NodeEnum.SPB', + 'data_reduction_percent': 0, + 'data_reduction_ratio': 1.0, + 'data_reduction_size_saved': 0, + 'default_node': 'NodeEnum.SPB', + 'description': None, + 'effective_io_limit_max_iops': None, + 'effective_io_limit_max_kbps': None, + 'existed': True, + 'family_base_lun': {'UnityLun': {'id': 'sv_1613'}}, + 'family_clone_count': 0, + 'hash': 8769317548849, + 'health': {'UnityHealth': {}}, + 'host_access': [], + 'id': 'sv_214551', + 'io_limit_policy': None, + 'is_advanced_dedup_enabled': False, + 'is_compression_enabled': True, + 'is_data_reduction_enabled': True, + 'is_replication_destination': False, + 'is_snap_schedule_paused': False, + 'is_thin_clone': True, + 'is_thin_enabled': True, + 'metadata_size': 3758096384, + 'metadata_size_allocated': 3221225472, + 'name': 'Atest', + 'per_tier_size_used': [3489660928, 0, 0], + 'pool': {'id': 'pool_3', 'name': 'Extreme_Perf_tier'}, + 'size_allocated': 0, + 'size_total': 2147483648, + 'size_total_with_unit': '2.0 GB', + 'size_used': None, + 'snap_count': 0, + 'snap_schedule': None, + 'snap_wwn': '60:06:01:60:5C:F0:50:00:F6:42:70:38:7A:90:40:FF', + 'snaps_size': 0, + 'snaps_size_allocated': 0, + 'storage_resource': {'UnityStorageResource': {'id': 'sv_1678', 'type': 8}}, + 'tiering_policy': 'TieringPolicyEnum.AUTOTIER_HIGH', + 'type': 'LUNTypeEnum.STANDALONE', + 'wwn': '60:06:01:60:5C:F0:50:00:41:25:EA:63:94:92:92:AE', + }} + else: + return None + + @staticmethod + def refresh_volume_response(response_type): + if response_type == 'api': + return {'copy': { + 'id': "85899345930" + }} + else: + return 'Failed to refresh thin clone [name: Atest, id: sv_214551] with error' diff --git a/tests/unit/plugins/modules/test_consistencygroup.py b/tests/unit/plugins/modules/test_consistencygroup.py index dd2cdd8..dea5876 100644 --- a/tests/unit/plugins/modules/test_consistencygroup.py +++ b/tests/unit/plugins/modules/test_consistencygroup.py @@ -191,3 +191,37 @@ def test_disable_cg_replication_throws_exception(self, consistencygroup_module_m consistencygroup_module_mock.perform_module_operation() assert consistencygroup_module_mock.module.fail_json.call_args[1]['msg'] == \ MockConsistenyGroupApi.get_cg_replication_dependent_response('disable_cg_exception') + + def test_refresh_cg(self, consistencygroup_module_mock): + self.get_module_args.update({ + 'cg_name': 'lun_test_cg_clone', + 'state': 'refreshed' + }) + consistencygroup_module_mock.module.params = self.get_module_args + cg_details = MockConsistenyGroupApi.cg_get_refreshable_details_method_response() + cg_object = MockConsistenyGroupApi.get_refreshable_cg_object() + refresh_resp = MockSDKObject(MockConsistenyGroupApi.refresh_cg_response('api')['copy']) + consistencygroup_module_mock.unity_conn.get_cg = MagicMock(return_value=cg_object) + consistencygroup_module_mock.get_details = MagicMock(return_value=cg_details) + cg_object.get_id = MagicMock(return_value=cg_details['id']) + utils.cg.UnityConsistencyGroup.get = MagicMock(return_value=cg_object) + cg_object.refresh = MagicMock(return_value=refresh_resp) + consistencygroup_module_mock.perform_module_operation() + assert consistencygroup_module_mock.module.exit_json.call_args[1]['changed'] is True + + def test_refresh_cg_throws_exception(self, consistencygroup_module_mock): + self.get_module_args.update({ + 'cg_name': 'lun_test_cg_clone', + 'state': 'refreshed' + }) + consistencygroup_module_mock.module.params = self.get_module_args + cg_details = MockConsistenyGroupApi.cg_get_refreshable_details_method_response() + cg_object = MockConsistenyGroupApi.get_refreshable_cg_object() + consistencygroup_module_mock.unity_conn.get_cg = MagicMock(return_value=cg_object) + consistencygroup_module_mock.get_details = MagicMock(return_value=cg_details) + cg_object.get_id = MagicMock(return_value=cg_details['id']) + utils.cg.UnityConsistencyGroup.get = MagicMock(return_value=cg_object) + cg_object.refresh = MagicMock(side_effect=MockApiException) + consistencygroup_module_mock.perform_module_operation() + assert MockConsistenyGroupApi.refresh_cg_response('error') in \ + consistencygroup_module_mock.module.fail_json.call_args[1]['msg'] diff --git a/tests/unit/plugins/modules/test_volume.py b/tests/unit/plugins/modules/test_volume.py index 1081f8c..04252bf 100644 --- a/tests/unit/plugins/modules/test_volume.py +++ b/tests/unit/plugins/modules/test_volume.py @@ -126,3 +126,35 @@ def test_modify_volume_exception(self, volume_module_mock): volume_module_mock.perform_module_operation() assert MockVolumeApi.modify_volume_response('error') in \ volume_module_mock.module.fail_json.call_args[1]['msg'] + + def test_refresh_volume(self, volume_module_mock): + self.get_module_args.update({ + 'vol_name': "Atest", + 'state': 'refreshed' + }) + volume_module_mock.module.params = self.get_module_args + volume_module_mock.host_access_modify_required = MagicMock(return_value=False) + obj_vol = MockSDKObject(MockVolumeApi.refreshable_volume_response('api')['volume_details']) + refresh_resp = MockVolumeApi.refresh_volume_response('api')['copy'] + volume_module_mock.unity_conn.get_lun = MagicMock(return_value=obj_vol) + obj_vol.refresh = MagicMock(return_value=MockSDKObject(refresh_resp)) + volume_module_mock.volume_modify_required = MagicMock() + volume_module_mock.get_volume_display_attributes = MagicMock() + volume_module_mock.perform_module_operation() + assert volume_module_mock.module.exit_json.call_args[1]['changed'] is True + + def test_refresh_volume_exception(self, volume_module_mock): + self.get_module_args.update({ + 'vol_name': "Atest", + 'state': 'refreshed' + }) + volume_module_mock.module.params = self.get_module_args + volume_module_mock.host_access_modify_required = MagicMock(return_value=False) + obj_vol = MockSDKObject(MockVolumeApi.refreshable_volume_response('api')['volume_details']) + volume_module_mock.unity_conn.get_lun = MagicMock(return_value=obj_vol) + obj_vol.refresh = MagicMock(side_effect=MockApiException) + volume_module_mock.volume_modify_required = MagicMock() + volume_module_mock.get_volume_display_attributes = MagicMock() + volume_module_mock.perform_module_operation() + assert MockVolumeApi.refresh_volume_response('error') in \ + volume_module_mock.module.fail_json.call_args[1]['msg']