diff --git a/PyPowerFlex/__init__.py b/PyPowerFlex/__init__.py index cd9ff40..dc41a5e 100644 --- a/PyPowerFlex/__init__.py +++ b/PyPowerFlex/__init__.py @@ -129,9 +129,9 @@ def add_objects_common(self): self.__add_storage_entity('host', common.Host) self.__add_storage_entity('utility', common.PowerFlexUtility) - def add_objects_gen1(self): """Add gen1 objects here.""" + self.__add_storage_entity('system', gen1.System) self.__add_storage_entity('device', gen1.Device) self.__add_storage_entity( 'fault_set', gen1.FaultSet) @@ -157,9 +157,11 @@ def add_objects_gen1(self): def add_objects_gen2(self): """Add gen2 objects here.""" + self.__add_storage_entity('system', gen2.System) self.__add_storage_entity('storage_node', gen2.StorageNode) self.__add_storage_entity('protection_domain', gen2.ProtectionDomain) self.__add_storage_entity('storage_pool', gen2.StoragePool) self.__add_storage_entity('snapshot_policy', gen2.SnapshotPolicy) self.__add_storage_entity('device', gen2.Device) self.__add_storage_entity('device_group', gen2.DeviceGroup) + self.__add_storage_entity('volume', gen2.Volume) diff --git a/PyPowerFlex/constants.py b/PyPowerFlex/constants.py index 891de6b..772d038 100644 --- a/PyPowerFlex/constants.py +++ b/PyPowerFlex/constants.py @@ -272,6 +272,22 @@ class VolumeConstants: "childVolumeIds", "userDataSdcWriteLatency"] +class VolumeConstantsGen2: + """ + This class holds constants related to Volume. + """ + DEFAULT_STATISTICS_METRICS = [ + "host_trim_bandwidth", + "host_trim_iops", + "avg_host_write_latency", + "logical_provisioned", + "avg_host_read_latency", + "host_read_bandwidth", + "host_read_iops", + "logical_used", + "host_write_bandwidth", + "host_write_iops", + "avg_host_trim_latency"] class RCGConstants: """ diff --git a/PyPowerFlex/objects/common/system.py b/PyPowerFlex/objects/common/system.py index 40b7957..e7587af 100644 --- a/PyPowerFlex/objects/common/system.py +++ b/PyPowerFlex/objects/common/system.py @@ -119,46 +119,6 @@ def remove_cg_snapshots(self, system_id, cg_id, allow_ext_managed=None): return response - def snapshot_volumes(self, - system_id, - snapshot_defs, - access_mode=None, - retention_period=None, - allow_ext_managed=None): - """Create snapshots of PowerFlex volumes. - - :type retention_period: str - :type access_mode: str - :type system_id: str - :type snapshot_defs: list[dict] - :type allow_ext_managed: bool - :rtype: dict - """ - - action = 'snapshotVolumes' - - params = { - 'snapshotDefs': snapshot_defs, - 'allowOnExtManagedVol': allow_ext_managed, - 'accessModeLimit': access_mode, - 'retentionPeriodInMin': retention_period - } - - r, response = self.send_post_request(self.base_action_url, - action=action, - entity=self.entity, - entity_id=system_id, - params=params) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to snapshot volumes on PowerFlex {self.entity} " - f"with id {system_id}. Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return response - def add_standby_mdm(self, mdm_ips, role, management_ips=None, port=None, mdm_name=None, allow_multiple_ips=None, clean=None, virtual_interface=None): diff --git a/PyPowerFlex/objects/common/utility.py b/PyPowerFlex/objects/common/utility.py index 0ea1c12..0a21355 100644 --- a/PyPowerFlex/objects/common/utility.py +++ b/PyPowerFlex/objects/common/utility.py @@ -26,6 +26,7 @@ from PyPowerFlex.constants import ( StoragePoolConstants, VolumeConstants, + VolumeConstantsGen2, SnapshotPolicyConstants, StorageNodeConstants ) @@ -111,6 +112,16 @@ def get_statistics_for_all_volumes(self, ids=None, properties=None): return response + def query_metrics_for_all_volumes_gen2(self, ids=None, metrics=None): + """list volume statistics for PowerFlex 5.0+. + + :param ids: list + :param metrics: list + :return: dict + """ + metrics = metrics or VolumeConstantsGen2.DEFAULT_STATISTICS_METRICS + return self.query_metrics('volume', ids, metrics) + def get_statistics_for_all_snapshot_policies( self, ids=None, properties=None): """list snapshot policy statistics for PowerFlex. diff --git a/PyPowerFlex/objects/gen1/__init__.py b/PyPowerFlex/objects/gen1/__init__.py index 5ebde98..85a52fa 100644 --- a/PyPowerFlex/objects/gen1/__init__.py +++ b/PyPowerFlex/objects/gen1/__init__.py @@ -21,6 +21,7 @@ from PyPowerFlex.objects.gen1.sds import Sds from PyPowerFlex.objects.gen1.snapshot_policy import SnapshotPolicy from PyPowerFlex.objects.gen1.storage_pool import StoragePool +from PyPowerFlex.objects.gen1.system import System from PyPowerFlex.objects.gen1.acceleration_pool import AccelerationPool from PyPowerFlex.objects.gen1.volume import Volume from PyPowerFlex.objects.gen1.replication_consistency_group import ReplicationConsistencyGroup @@ -38,6 +39,7 @@ 'Sds', 'SnapshotPolicy', 'StoragePool', + 'System', 'AccelerationPool', 'Volume', 'ReplicationConsistencyGroup', diff --git a/PyPowerFlex/objects/gen1/system.py b/PyPowerFlex/objects/gen1/system.py new file mode 100644 index 0000000..d483f6a --- /dev/null +++ b/PyPowerFlex/objects/gen1/system.py @@ -0,0 +1,70 @@ +# Copyright (c) 2024 Dell Inc. or its subsidiaries. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Module for doing system-related operations.""" + +# pylint: disable=no-member,too-many-arguments,too-many-positional-arguments,duplicate-code + +import logging + +import requests + +from PyPowerFlex import exceptions +from PyPowerFlex.objects.common.system import System as SystemCommon + +LOG = logging.getLogger(__name__) + + +class System(SystemCommon): + """Client for system operations""" + def snapshot_volumes(self, + system_id, + snapshot_defs, + access_mode=None, + retention_period=None, + allow_ext_managed=None): + """Create snapshots of PowerFlex volumes. + + :type retention_period: str + :type access_mode: str + :type system_id: str + :type snapshot_defs: list[dict] + :type allow_ext_managed: bool + :rtype: dict + """ + + action = 'snapshotVolumes' + + params = { + 'snapshotDefs': snapshot_defs, + 'allowOnExtManagedVol': allow_ext_managed, + 'accessModeLimit': access_mode, + 'retentionPeriodInMin': retention_period + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=system_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to snapshot volumes on PowerFlex {self.entity} " + f"with id {system_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response diff --git a/PyPowerFlex/objects/gen2/__init__.py b/PyPowerFlex/objects/gen2/__init__.py index 36a9002..b36b453 100644 --- a/PyPowerFlex/objects/gen2/__init__.py +++ b/PyPowerFlex/objects/gen2/__init__.py @@ -21,6 +21,8 @@ from PyPowerFlex.objects.gen2.snapshot_policy import SnapshotPolicy from PyPowerFlex.objects.gen2.device import Device from PyPowerFlex.objects.gen2.device_group import DeviceGroup +from PyPowerFlex.objects.gen2.volume import Volume +from PyPowerFlex.objects.gen2.system import System __all__ = [ 'StorageNode', @@ -29,4 +31,6 @@ 'SnapshotPolicy', 'Device', 'DeviceGroup', + 'Volume', + 'System' ] diff --git a/PyPowerFlex/objects/gen2/system.py b/PyPowerFlex/objects/gen2/system.py new file mode 100644 index 0000000..f5760a9 --- /dev/null +++ b/PyPowerFlex/objects/gen2/system.py @@ -0,0 +1,82 @@ +# Copyright (c) 2024 Dell Inc. or its subsidiaries. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Module for doing system-related operations.""" + +# pylint: disable=no-member,too-many-arguments,too-many-positional-arguments,duplicate-code + +import logging +import requests + +from PyPowerFlex import exceptions +from PyPowerFlex.objects.common.system import System as SystemCommon + +LOG = logging.getLogger(__name__) + + +class System(SystemCommon): + """Client for system operations""" + + def create_snapshot(self, + system_id, + snapshot_defs, + retention_period=None): + """Create a snapshot in Gen2.""" + action = 'createSnapshot' + + params = { + 'snapshotDefs': snapshot_defs, + 'retentionPeriodInMin': retention_period + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=system_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to create snapshot on PowerFlex {self.entity} " + f"with id {system_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response + + def create_thin_clone(self, + system_id, + snapshot_defs): + """Create a thin clone in Gen2.""" + action = 'createThinClone' + + params = { + 'snapshotDefs': snapshot_defs + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=system_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to create thin clone on PowerFlex {self.entity} " + f"with id {system_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response diff --git a/PyPowerFlex/objects/gen2/volume.py b/PyPowerFlex/objects/gen2/volume.py new file mode 100644 index 0000000..7d6ad55 --- /dev/null +++ b/PyPowerFlex/objects/gen2/volume.py @@ -0,0 +1,417 @@ +# Copyright (c) 2024 Dell Inc. or its subsidiaries. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Module for interacting with volume APIs for Gen2.""" + +# pylint: disable=too-few-public-methods,no-member,too-many-arguments,too-many-positional-arguments,duplicate-code + +import logging + +import requests + +from PyPowerFlex import base_client +from PyPowerFlex import exceptions +from PyPowerFlex.constants import VolumeConstantsGen2 + +LOG = logging.getLogger(__name__) + + +class RemoveMode: + """Volume remove modes. + + Represents volume deletion strategy. See PowerFlex documentation for more + information. + """ + + only_me = 'ONLY_ME' + including_descendants = 'INCLUDING_DESCENDANTS' + descendants_only = 'DESCENDANTS_ONLY' + whole_vtree = 'WHOLE_VTREE' + + +class VolumeType: + """Volume provisioning types.""" + + thick = 'ThickProvisioned' + thin = 'ThinProvisioned' + + +class VolumeClass: + """Volume class types.""" + + supported_vol_classes = ( + ['defaultclass', 'replication', 'csi', 'openstack', 'vvol', 'datastore', + 'nasfs', 'nasvdm', 'nascluster', 'nas']) + for vol_class in supported_vol_classes: + locals()[vol_class] = vol_class + + +class Volume(base_client.EntityRequest): + """ + A class representing Volume client. + Note that this class could also be used for snapshot and thin clone. + """ + def add_mapped_host(self, + volume_id, + host_id=None, + guid=None, + nqn=None, + allow_multiple_mappings=None, + access_mode=None, + volume_class=VolumeClass.defaultclass): + """Map PowerFlex volume to host. + + :param volume_id: str + :param host_id: str + :param guid: str + :param nqn: str + :param allow_multiple_mappings: bool + :type access_mode: str + :param volume_class: str + :return: dict + """ + + action = 'addMappedHost' + + params = { + "hostId": host_id, + "guid": guid, + "nqn": nqn, + "allowMultipleMappings": allow_multiple_mappings, + "accessMode": access_mode, + "volumeClass": volume_class + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=volume_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to map PowerFlex {self.entity} with id {volume_id} " + f"to host. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def create(self, + storage_pool_id, + size_in_gb, + name=None, + volume_type=None, + use_rmcache=None, + volume_class=VolumeClass.defaultclass): + """Create PowerFlex volume. + + :param storage_pool_id: str + :param size_in_gb: int + :param name: str + :param volume_type: one of predefined attributes of VolumeType + :type volume_type: str + :param use_rmcache: bool + :param volume_class: str + :return: dict + """ + + params = { + 'storagePoolId': storage_pool_id, + 'volumeSizeInGb': size_in_gb, + 'name': name, + 'volumeType': volume_type, + 'useRmcache': use_rmcache, + 'volumeClass': volume_class + } + + return self._create_entity(params) + + def delete(self, volume_id, remove_mode, + volume_class=VolumeClass.defaultclass): + """Remove PowerFlex volume/snapshot/thin clone. + + :param volume_id: str + :param remove_mode: one of predefined attributes of RemoveMode + :param volume_class: str + :return: None + """ + + params = { + "removeMode": remove_mode, + "volumeClass": volume_class + } + + return self._delete_entity(volume_id, params) + + def extend(self, volume_id, size_in_gb, + volume_class=VolumeClass.defaultclass): + """Extend PowerFlex volume/thin clone. + + :param volume_id: str + :param size_in_gb: int + :param volume_class: str + :return: dict + """ + + action = 'setVolumeSize' + + params = {"sizeInGB": size_in_gb, + "volumeClass": volume_class} + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=volume_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to extend PowerFlex {self.entity} with id {volume_id}. " + f"Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def get_statistics(self, volume_id, metrics=None): + """Get related PowerFlex Statistics for volume. + + :type volume_id: str + :type metrics: list|tuple + :rtype: dict + """ + + metrics = metrics or VolumeConstantsGen2.DEFAULT_STATISTICS_METRICS + return self.query_metrics('volume', volume_id, metrics) + + def remove_mapped_host(self, + volume_id, + host_id=None, + guid=None, + nqn=None, + all_hosts=None, + volume_class=VolumeClass.defaultclass): + """Unmap PowerFlex volume from host. + + :param volume_id: str + :param host_id: str + :param guid: str + :param nqn: str + :param all_hosts: bool + :param volume_class: str + :return: dict + """ + + action = 'removeMappedHost' + + params = { + "hostId": host_id, + "guid": guid, + "nqn": nqn, + "allHosts": all_hosts, + "volumeClass": volume_class + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=volume_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to unmap PowerFlex {self.entity} with id {volume_id} from " + f"host. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def rename(self, volume_id, name, + volume_class=VolumeClass.defaultclass): + """Rename PowerFlex volume/snapshot/thin clone. + + :param volume_id: str + :param name: str + :param volume_class: str + :return: dict + """ + + action = 'setVolumeName' + + params = { + "newName": name, + "volumeClass": volume_class + } + + return self._rename_entity(action, volume_id, params) + + def set_mapped_sdc_limits(self, volume_id, sdc_id, bandwidth_limit=None, + iops_limit=None): + """Set the bandwidth limit and IOPS limit for the mapped SDC. + + :param volume_id: ID of the volume + :type volume_id: str + :param sdc_id: ID of the SDC + :type sdc_id: str + :param bandwidth_limit: Limit for the volume network bandwidth + :type bandwidth_limit: str + :param iops_limit: Limit for the volume IOPS + :type iops_limit: str + :return: dict + """ + + action = 'setMappedSdcLimits' + + params = { + "sdcId": sdc_id, + "bandwidthLimitInKbps": bandwidth_limit, + "iopsLimit": iops_limit + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=volume_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to update the SDC limits of PowerFlex " + f"{self.entity} with id {volume_id}. " + f"Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def set_access_mode_for_sdc(self, volume_id, sdc_id, access_mode): + """ + Set the volume access mode for the specified + SDC mapped to the volume. + + :param volume_id: ID of the volume + :type volume_id: str + :param access_mode: The access mode of the volume for the mapped SDC + :type access_mode: str + :param sdc_id: ID of the SDC. + :type sdc_id: str + :return: dict + """ + + action = 'setVolumeMappingAccessMode' + + params = { + "accessMode": access_mode, + "sdcId": sdc_id + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=volume_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to set the access mode for the SDC {sdc_id} " + f"mapped to PowerFlex {self.entity} with id {volume_id}. Error: " + f"{response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def set_retention_period(self, snap_id, retention_period): + """ + Set a new retention period for the given snapshot. If the snapshot + is already secure, then it can be delayed but not advanced. + + :param snap_id: ID of the volume + :type snap_id: str + :param retention_period: Retention period for the specified resource + :type retention_period: str + :return: dict + """ + + action = 'setSnapshotSecurity' + + params = { + "retentionPeriodInMin": retention_period, + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=snap_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to set the retention period for PowerFlex {self.entity} " + f"with id {snap_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def refresh(self, dest_vol_id, src_vol_id): + """ + Refresh a destination volume from a source volume. + + :param dest_vol_id: ID of the destination volume + :type dest_vol_id: str + :param src_vol_id: ID of the source volume + :type src_vol_id: str + :return: dict + """ + + action = 'refresh' + + params = { + "srcVolumeId": src_vol_id + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=dest_vol_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to refresh PowerFlex {self.entity} " + f"with id {dest_vol_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def restore(self, dest_vol_id, src_vol_id): + """ + Restore a destination volume from a source volume. + + :param dest_vol_id: ID of the destination volume + :type dest_vol_id: str + :param src_vol_id: ID of the source volume + :type src_vol_id: str + :return: dict + """ + + action = 'restore' + + params = { + "srcVolumeId": src_vol_id + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=dest_vol_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to restore PowerFlex {self.entity} " + f"with id {dest_vol_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) diff --git a/tests/common/test_utility.py b/tests/common/test_utility.py index 9626f4d..a50f8f8 100644 --- a/tests/common/test_utility.py +++ b/tests/common/test_utility.py @@ -72,6 +72,12 @@ def test_get_statistics_for_all_volumes_bad_status(self): self.assertRaises(exceptions.PowerFlexClientException, self.client.utility.get_statistics_for_all_volumes) + def test_query_metrics_for_all_volumes_gen2(self): + """ + Test the test_query_metrics_for_all_volumes_gen2 method. + """ + self.client.utility.query_metrics_for_all_volumes_gen2() + def test_query_metrics_for_all_storage_nodes(self): """ Test the query_metrics_for_all_storage_nodes method. diff --git a/tests/common/test_system.py b/tests/gen1/test_system.py similarity index 98% rename from tests/common/test_system.py rename to tests/gen1/test_system.py index ffe7073..2cf19a8 100644 --- a/tests/common/test_system.py +++ b/tests/gen1/test_system.py @@ -22,6 +22,7 @@ from tests.common import PyPowerFlexTestCase +@PyPowerFlexTestCase.version('4.5') class TestSystemClient(PyPowerFlexTestCase): """ Test class for the SystemClient. @@ -93,7 +94,7 @@ def test_system_api_version(self): Test the API version. """ self.client.system.api_version() - self.assertEqual(4, self.get_mock.call_count) + self.assertEqual(8, self.get_mock.call_count) def test_system_api_version_bad_status(self): """ @@ -120,7 +121,7 @@ def test_system_api_version_cached(self): self.client.system.api_version() self.client.system.api_version() self.client.system.api_version() - self.assertEqual(4, self.get_mock.call_count) + self.assertEqual(8, self.get_mock.call_count) def test_system_remove_cg_snapshots(self): """ diff --git a/tests/gen2/test_system.py b/tests/gen2/test_system.py new file mode 100644 index 0000000..26bcbab --- /dev/null +++ b/tests/gen2/test_system.py @@ -0,0 +1,358 @@ +# Copyright (c) 2024 Dell Inc. or its subsidiaries. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Module for testing system client.""" + +# pylint: disable=invalid-name,too-many-public-methods,duplicate-code + +from PyPowerFlex import exceptions +from PyPowerFlex.objects.common import system +from tests.common import PyPowerFlexTestCase + + +@PyPowerFlexTestCase.version('5.0') +class TestSystemClient(PyPowerFlexTestCase): + """ + Test class for the SystemClient. + """ + def setUp(self): + """ + Set up the test environment. + """ + super().setUp() + self.client.initialize() + self.fake_system_id = '1' + self.fake_cg_id = '1' + self.fake_snapshot_defs = [system.SnapshotDef('123', 'snap_1')] + self.fake_mdm_id = '1' + + self.MOCK_RESPONSES = { + self.RESPONSE_MODE.Valid: { + f'/instances/System::{self.fake_system_id}' + '/action/removeConsistencyGroupSnapshots': + {}, + f'/instances/System::{self.fake_system_id}' + '/action/createSnapshot': + {}, + f'/instances/System::{self.fake_system_id}' + '/action/createThinClone': + {}, + '/instances/System' + '/action' + '/addStandbyMdm': + {}, + '/instances/System' + '/action' + '/removeStandbyMdm': + {}, + '/instances/System' + '/action' + '/changeMdmOwnership': + {}, + '/instances/System' + '/action' + '/setMdmPerformanceParameters': + {}, + '/instances/System' + '/action' + '/renameMdm': + {}, + '/instances/System' + '/action' + '/modifyVirtualIpInterfaces': + {}, + '/instances/System' + '/action' + '/switchClusterMode': + {}, + '/instances/System' + '/queryMdmCluster': + {}, + '/Configuration': + {}, + '/types/System' + '/instances/action/querySelectedStatistics': { + 'rplTransmitBwc': {'numSeconds': 0, 'totalWeightInKb': 0, 'numOccured': 0} + }, + }, + self.RESPONSE_MODE.Invalid: { + '/version': 'invalid_version_format' + }, + } + + def test_system_api_version(self): + """ + Test the API version. + """ + self.client.system.api_version() + self.assertEqual(8, self.get_mock.call_count) + + def test_system_api_version_bad_status(self): + """ + Test the API version with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexFailQuerying, + self.client.system.api_version, + cached=False) + + def test_system_api_version_invalid_format(self): + """ + Test the API version with an invalid format. + """ + with self.http_response_mode(self.RESPONSE_MODE.Invalid): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.system.api_version, + cached=False) + + def test_system_api_version_cached(self): + """ + Test the API version with caching. + """ + self.client.system.api_version() + self.client.system.api_version() + self.client.system.api_version() + self.assertEqual(8, self.get_mock.call_count) + + def test_system_remove_cg_snapshots(self): + """ + Test removing consistency group snapshots. + """ + self.client.system.remove_cg_snapshots(self.fake_system_id, + self.fake_cg_id) + + def test_system_remove_cg_snapshots_bad_status(self): + """ + Test removing consistency group snapshots with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.system.remove_cg_snapshots, + self.fake_system_id, + self.fake_cg_id) + + def test_snapshot_create(self): + """ + Test the create_snapshot method. + """ + self.client.system.create_snapshot(self.fake_system_id, + self.fake_snapshot_defs, + retention_period=10) + + def test_snapshot_create_bad_status(self): + """ + Test the create_snapshot method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.system.create_snapshot, + self.fake_system_id, + self.fake_snapshot_defs, + retention_period=10) + + def test_thin_clone_create(self): + """ + Test the create_thin_clone method. + """ + self.client.system.create_thin_clone(self.fake_system_id, + self.fake_snapshot_defs) + + def test_thin_clone_create_bad_status(self): + """ + Test the create_thin_clone method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.system.create_thin_clone, + self.fake_system_id, + self.fake_snapshot_defs) + + def test_add_standby_mdm(self): + """ + Test the add_standby_mdm method. + """ + self.client.system.add_standby_mdm(mdm_ips=["10.x.x.x"], + role="Manager") + + def test_add_standby_mdm_bad_status(self): + """ + Test the add_standby_mdm method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.system.add_standby_mdm, + mdm_ips=["10.x.x.x"], role="Manager") + + def test_remove_standby_mdm(self): + """ + Test the remove_standby_mdm method. + """ + self.client.system.remove_standby_mdm(self.fake_mdm_id) + + def test_remove_standby_mdm_bad_status(self): + """ + Test the remove_standby_mdm method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.system.remove_standby_mdm, + self.fake_mdm_id) + + def test_get_mdm_cluster(self): + """ + Test the get_mdm_cluster_details method. + """ + self.client.system.get_mdm_cluster_details() + + def test_get_mdm_cluster_bad_status(self): + """ + Test the get_mdm_cluster_details method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.system.get_mdm_cluster_details) + + def test_change_mdm_ownership(self): + """ + Test the change_mdm_ownership method. + """ + self.client.system.change_mdm_ownership(self.fake_mdm_id) + + def test_change_mdm_ownership_bad_status(self): + """ + Test the change_mdm_ownership method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.system.change_mdm_ownership, + self.fake_mdm_id) + + def test_change_performance_profile(self): + """ + Test the set_cluster_mdm_performance_profile method. + """ + self.client.system.\ + set_cluster_mdm_performance_profile(performance_profile="Compact") + + def test_change_performance_profile_bad_status(self): + """ + Test the set_cluster_mdm_performance_profile method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.system. + set_cluster_mdm_performance_profile, + performance_profile="Compact") + + def test_rename_mdm(self): + """ + Test the rename_mdm method. + """ + self.client.system.rename_mdm(self.fake_mdm_id, mdm_new_name="fake") + + def test_rename_mdm_bad_status(self): + """ + Test the rename_mdm method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.system.rename_mdm, + self.fake_mdm_id, mdm_new_name="fake") + + def test_modify_virtual_ip_interface(self): + """ + Test the modify_virtual_ip_interface method. + """ + self.client.system.\ + modify_virtual_ip_interface(self.fake_mdm_id, + virtual_ip_interfaces=["interface"]) + + def test_modify_virtual_ip_interface_bad_status(self): + """ + Test the modify_virtual_ip_interface method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.system.modify_virtual_ip_interface, + self.fake_mdm_id, + virtual_ip_interfaces=["interface"]) + + def test_clear_virtual_ip_interface(self): + """ + Test the modify_virtual_ip_interface method with no arguments. + """ + self.client.system.modify_virtual_ip_interface(self.fake_mdm_id) + + def test_clear_virtual_ip_interface_bad_status(self): + """ + Test the modify_virtual_ip_interface method with no arguments and a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.system.modify_virtual_ip_interface, + self.fake_mdm_id) + + def test_switch_cluster_mode(self): + """ + Test the switch_cluster_mode method. + """ + self.client.system.switch_cluster_mode(self.fake_mdm_id) + + def test_switch_cluster_mode_bad_status(self): + """ + Test the switch_cluster_mode method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.system.switch_cluster_mode, + self.fake_mdm_id) + + def test_get_gateway_configuration_details(self): + """ + Test the get_gateway_configuration_details method. + """ + self.client.system.get_gateway_configuration_details() + + def test_get_gateway_configuration_details_bad_status(self): + """ + Test the get_gateway_configuration_details method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.system.get_gateway_configuration_details) + + def test_system_query_selected_statistics(self): + """ + Test the query_selected_statistics method. + """ + ret = self.client.system.query_selected_statistics( + properties=["rplTransmitBwc"] + ) + assert ret.get("rplTransmitBwc") == { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0, + } + + def test_system_query_selected_statistics_bad_status(self): + """ + Test the query_selected_statistics method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises( + exceptions.PowerFlexFailQuerying, + self.client.system.query_selected_statistics, + properties=["rplTransmitBwc"], + ) diff --git a/tests/gen2/test_volume.py b/tests/gen2/test_volume.py new file mode 100644 index 0000000..fd3c56d --- /dev/null +++ b/tests/gen2/test_volume.py @@ -0,0 +1,292 @@ +# Copyright (c) 2024 Dell Inc. or its subsidiaries. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Module for testing volume client in Gen2.""" + +# pylint: disable=invalid-name,too-many-public-methods,duplicate-code + +from PyPowerFlex import exceptions +from PyPowerFlex.objects.gen2 import volume +from tests.common import PyPowerFlexTestCase + +@PyPowerFlexTestCase.version('5.0') +class TestVolumeClient(PyPowerFlexTestCase): + """ + Test class for the volume client in Gen2. + """ + + def setUp(self): + """ + Set up the test case. + """ + super().setUp() + self.client.initialize() + self.fake_sp_id = '1' + self.fake_volume_id = '1' + self.fake_snapshot_id = '1' + + self.MOCK_RESPONSES = { + self.RESPONSE_MODE.Valid: { + '/types/Volume/instances': + {'id': self.fake_volume_id}, + f'/instances/Volume::{self.fake_volume_id}': + {'id': self.fake_volume_id}, + f'/instances/Volume::{self.fake_volume_id}/action/removeVolume': + {}, + f'/instances/Volume::{self.fake_volume_id}/action/setVolumeSize': + {}, + f'/instances/Volume::{self.fake_volume_id}/action/addMappedHost': + {}, + f'/instances/Volume::{self.fake_volume_id}/action/removeMappedHost': + {}, + f'/instances/Volume::{self.fake_volume_id}/action/setMappedSdcLimits': + {}, + f'/instances/Volume::{self.fake_volume_id}/action/setVolumeMappingAccessMode': + {}, + f'/instances/Volume::{self.fake_volume_id}/action/setVolumeName': + {}, + f'/instances/Volume::{self.fake_volume_id}/action/refresh': + {}, + f'/instances/Volume::{self.fake_volume_id}/action/restore': + {}, + f'/instances/Volume::{self.fake_volume_id}/action/setSnapshotSecurity': + {}, + '/dtapi/rest/v1/metrics/query': + {}, + }, + self.RESPONSE_MODE.Invalid: { + '/types/Volume/instances': + {}, + } + } + + def test_volume_add_mapped_sdc(self): + """ + Test if volume add mapped sdc is successful. + """ + self.client.volume.add_mapped_host(self.fake_volume_id, + host_id='1') + + def test_volume_add_mapped_sdc_bad_status(self): + """ + Test if volume add mapped sdc raises an exception when the HTTP status is bad. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.volume.add_mapped_host, + self.fake_volume_id, + host_id='1') + + def test_volume_create(self): + """ + Test if volume create is successful. + """ + self.client.volume.create(size_in_gb=8, + storage_pool_id=self.fake_sp_id, + volume_type=volume.VolumeType.thin) + + def test_volume_create_bad_status(self): + """ + Test if volume create raises an exception when the HTTP status is bad. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexFailCreating, + self.client.volume.create, + size_in_gb=8, + storage_pool_id=self.fake_sp_id, + volume_type=volume.VolumeType.thin) + + def test_volume_create_no_id_in_response(self): + """ + Test if volume create raises an exception when the response does not contain an id. + """ + with self.http_response_mode(self.RESPONSE_MODE.Invalid): + self.assertRaises(KeyError, + self.client.volume.create, + size_in_gb=8, + storage_pool_id=self.fake_sp_id, + volume_type=volume.VolumeType.thin) + + def test_volume_delete(self): + """ + Test if volume delete is successful. + """ + self.client.volume.delete(self.fake_volume_id, + remove_mode=volume.RemoveMode.only_me) + + def test_volume_delete_bad_status(self): + """ + Test if volume delete raises an exception when the HTTP status is bad. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexFailDeleting, + self.client.volume.delete, + self.fake_volume_id, + remove_mode=volume.RemoveMode.only_me) + + def test_volume_extend(self): + """ + Test if volume extend is successful. + """ + self.client.volume.extend(self.fake_volume_id, + size_in_gb=16) + + def test_volume_extend_bad_status(self): + """ + Test if volume extend raises an exception when the HTTP status is bad. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.volume.extend, + self.fake_volume_id, + size_in_gb=16) + + def test_volume_get_statistics(self): + """ + Test if volume get statistics is successful. + """ + self.client.volume.get_statistics(self.fake_volume_id) + + def test_volume_get_statistics_bad_status(self): + """ + Test if volume get statistics raises an exception when the HTTP status is bad. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.volume.get_statistics, + self.fake_volume_id) + + def test_volume_remove_mapped_sdc(self): + """ + Test the remove_mapped_sdc method. + """ + self.client.volume.remove_mapped_host(self.fake_volume_id, + host_id='1') + + def test_volume_remove_mapped_sdc_bad_status(self): + """ + Test the remove_mapped_sdc method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.volume.remove_mapped_host, + self.fake_volume_id, + host_id='1') + + def test_volume_rename(self): + """ + Test the rename method. + """ + self.client.volume.rename(self.fake_volume_id, + name='new_name') + + def test_volume_rename_bad_status(self): + """ + Test the rename method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexFailRenaming, + self.client.volume.rename, + self.fake_volume_id, + name='new_name') + + def test_volume_set_mapped_sdc_limits(self): + """ + Test the set_mapped_sdc_limits method. + """ + self.client.volume.set_mapped_sdc_limits(self.fake_volume_id, + sdc_id='1', + bandwidth_limit='1') + + def test_volume_set_mapped_sdc_limits_bad_status(self): + """ + Test the set_mapped_sdc_limits method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.volume.set_mapped_sdc_limits, + self.fake_volume_id, + sdc_id='1', + bandwidth_limit='1') + + def test_volume_set_access_mode_for_sdc(self): + """ + Test the set_access_mode_for_sdc method. + """ + self.client.volume.set_access_mode_for_sdc(self.fake_volume_id, + sdc_id='1', + access_mode='ReadWrite') + + def test_volume_set_access_mode_for_sdc_bad_status(self): + """ + Test the set_access_mode_for_sdc method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.volume.set_access_mode_for_sdc, + self.fake_volume_id, + sdc_id='1', + access_mode='ReadWrite') + + def test_set_retention_period(self): + """ + Test the set_retention_period method. + """ + self.client.volume.set_retention_period(self.fake_snapshot_id, + retention_period='1') + + def test_set_retention_period_bad_status(self): + """ + Test the set_retention_period method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.volume.set_retention_period, + self.fake_snapshot_id, + retention_period='1') + + def test_volume_refresh(self): + """ + Test the refresh method. + """ + self.client.volume.refresh(dest_vol_id=self.fake_volume_id, + src_vol_id='1') + + def test_volume_refresh_bad_status(self): + """ + Test the refresh method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.volume.refresh, + dest_vol_id=self.fake_volume_id, + src_vol_id='1') + + def test_volume_restore(self): + """ + Test the restore method. + """ + self.client.volume.restore(dest_vol_id=self.fake_volume_id, + src_vol_id='1') + + def test_volume_restore_bad_status(self): + """ + Test the restore method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.volume.restore, + dest_vol_id=self.fake_volume_id, + src_vol_id='1')