From f6312ea4f17b2a48e365c344cc5033e87ca8dc57 Mon Sep 17 00:00:00 2001 From: Yiming Bao Date: Tue, 22 Jul 2025 17:50:51 +0800 Subject: [PATCH 01/15] Refactor - Support PowerFlex 5.0 --- PyPowerFlex/__init__.py | 75 ++- PyPowerFlex/gen1/objects/__init__.py | 59 ++ .../{ => gen1}/objects/acceleration_pool.py | 0 PyPowerFlex/{ => gen1}/objects/deployment.py | 0 PyPowerFlex/{ => gen1}/objects/device.py | 0 PyPowerFlex/{ => gen1}/objects/fault_set.py | 0 .../{ => gen1}/objects/firmware_repository.py | 0 PyPowerFlex/{ => gen1}/objects/host.py | 0 .../{ => gen1}/objects/managed_device.py | 0 .../{ => gen1}/objects/protection_domain.py | 0 .../objects/replication_consistency_group.py | 0 .../{ => gen1}/objects/replication_pair.py | 0 PyPowerFlex/{ => gen1}/objects/sdc.py | 0 PyPowerFlex/{ => gen1}/objects/sds.py | 0 PyPowerFlex/{ => gen1}/objects/sdt.py | 0 .../{ => gen1}/objects/service_template.py | 0 .../{ => gen1}/objects/snapshot_policy.py | 0 .../{ => gen1}/objects/storage_pool.py | 2 +- PyPowerFlex/{ => gen1}/objects/system.py | 0 PyPowerFlex/{ => gen1}/objects/utility.py | 0 PyPowerFlex/{ => gen1}/objects/volume.py | 0 PyPowerFlex/gen2/objects/__init__.py | 32 + PyPowerFlex/gen2/objects/sdc.py | 94 +++ PyPowerFlex/gen2/objects/sdt.py | 345 +++++++++++ PyPowerFlex/gen2/objects/storage_node.py | 244 ++++++++ PyPowerFlex/gen2/objects/system.py | 459 ++++++++++++++ PyPowerFlex/gen2/objects/utility.py | 140 +++++ PyPowerFlex/gen2/objects/volume.py | 574 ++++++++++++++++++ PyPowerFlex/objects/__init__.py | 59 -- setup.py | 7 +- tests/__init__.py | 233 ------- tests/gen1/__init__.py | 233 +++++++ tests/{ => gen1}/test_acceleration_pool.py | 8 +- tests/{ => gen1}/test_base.py | 4 +- tests/{ => gen1}/test_deployment.py | 5 +- tests/{ => gen1}/test_device.py | 6 +- tests/{ => gen1}/test_fault_set.py | 4 +- tests/{ => gen1}/test_firmware_repository.py | 4 +- tests/{ => gen1}/test_host.py | 4 +- tests/{ => gen1}/test_managed_device.py | 4 +- tests/{ => gen1}/test_protection_domain.py | 6 +- .../test_replication_consistency_group.py | 4 +- tests/{ => gen1}/test_replication_pair.py | 4 +- tests/{ => gen1}/test_sdc.py | 4 +- tests/{ => gen1}/test_sds.py | 6 +- tests/{ => gen1}/test_sdt.py | 6 +- tests/{ => gen1}/test_service_template.py | 4 +- tests/{ => gen1}/test_snapshot_policy.py | 14 +- tests/{ => gen1}/test_storage_pool.py | 10 +- tests/{ => gen1}/test_system.py | 6 +- tests/{ => gen1}/test_utility.py | 4 +- tests/{ => gen1}/test_volume.py | 8 +- tests/gen2/__init__.py | 0 tests/gen2/test_storage_node.py | 313 ++++++++++ 54 files changed, 2602 insertions(+), 382 deletions(-) create mode 100644 PyPowerFlex/gen1/objects/__init__.py rename PyPowerFlex/{ => gen1}/objects/acceleration_pool.py (100%) rename PyPowerFlex/{ => gen1}/objects/deployment.py (100%) rename PyPowerFlex/{ => gen1}/objects/device.py (100%) rename PyPowerFlex/{ => gen1}/objects/fault_set.py (100%) rename PyPowerFlex/{ => gen1}/objects/firmware_repository.py (100%) rename PyPowerFlex/{ => gen1}/objects/host.py (100%) rename PyPowerFlex/{ => gen1}/objects/managed_device.py (100%) rename PyPowerFlex/{ => gen1}/objects/protection_domain.py (100%) rename PyPowerFlex/{ => gen1}/objects/replication_consistency_group.py (100%) rename PyPowerFlex/{ => gen1}/objects/replication_pair.py (100%) rename PyPowerFlex/{ => gen1}/objects/sdc.py (100%) rename PyPowerFlex/{ => gen1}/objects/sds.py (100%) rename PyPowerFlex/{ => gen1}/objects/sdt.py (100%) rename PyPowerFlex/{ => gen1}/objects/service_template.py (100%) rename PyPowerFlex/{ => gen1}/objects/snapshot_policy.py (100%) rename PyPowerFlex/{ => gen1}/objects/storage_pool.py (99%) rename PyPowerFlex/{ => gen1}/objects/system.py (100%) rename PyPowerFlex/{ => gen1}/objects/utility.py (100%) rename PyPowerFlex/{ => gen1}/objects/volume.py (100%) create mode 100644 PyPowerFlex/gen2/objects/__init__.py create mode 100644 PyPowerFlex/gen2/objects/sdc.py create mode 100644 PyPowerFlex/gen2/objects/sdt.py create mode 100644 PyPowerFlex/gen2/objects/storage_node.py create mode 100644 PyPowerFlex/gen2/objects/system.py create mode 100644 PyPowerFlex/gen2/objects/utility.py create mode 100644 PyPowerFlex/gen2/objects/volume.py delete mode 100644 PyPowerFlex/objects/__init__.py create mode 100644 tests/gen1/__init__.py rename tests/{ => gen1}/test_acceleration_pool.py (95%) rename tests/{ => gen1}/test_base.py (98%) rename tests/{ => gen1}/test_deployment.py (98%) rename tests/{ => gen1}/test_device.py (97%) rename tests/{ => gen1}/test_fault_set.py (98%) rename tests/{ => gen1}/test_firmware_repository.py (95%) rename tests/{ => gen1}/test_host.py (97%) rename tests/{ => gen1}/test_managed_device.py (95%) rename tests/{ => gen1}/test_protection_domain.py (98%) rename tests/{ => gen1}/test_replication_consistency_group.py (99%) rename tests/{ => gen1}/test_replication_pair.py (98%) rename tests/{ => gen1}/test_sdc.py (98%) rename tests/{ => gen1}/test_sds.py (98%) rename tests/{ => gen1}/test_sdt.py (98%) rename tests/{ => gen1}/test_service_template.py (96%) rename tests/{ => gen1}/test_snapshot_policy.py (95%) rename tests/{ => gen1}/test_storage_pool.py (98%) rename tests/{ => gen1}/test_system.py (98%) rename tests/{ => gen1}/test_utility.py (96%) rename tests/{ => gen1}/test_volume.py (98%) create mode 100644 tests/gen2/__init__.py create mode 100644 tests/gen2/test_storage_node.py diff --git a/PyPowerFlex/__init__.py b/PyPowerFlex/__init__.py index 943951d..e878829 100644 --- a/PyPowerFlex/__init__.py +++ b/PyPowerFlex/__init__.py @@ -21,10 +21,10 @@ from PyPowerFlex import configuration from PyPowerFlex import exceptions -from PyPowerFlex import objects from PyPowerFlex import token from PyPowerFlex import utils - +import PyPowerFlex.gen1.objects as gen1 +import PyPowerFlex.gen2.objects as gen2 __all__ = [ 'PowerFlexClient' @@ -39,6 +39,7 @@ class PowerFlexClient: access to the various storage entities available in the PowerFlex system. """ __slots__ = ( + # gen1 '__is_initialized', 'configuration', 'token', @@ -60,7 +61,9 @@ class PowerFlexClient: 'managed_device', 'deployment', 'firmware_repository', - 'host' + 'host', + # gen2 + 'storage_node', ) def __init__(self, @@ -98,36 +101,52 @@ def initialize(self): Raises: PowerFlexClientException: If the PowerFlex API version is lower than 3.0. """ + # unchanged resources here + self.__add_storage_entity('system', gen1.System) + self.__add_storage_entity('sdc', gen1.Sdc) + self.__add_storage_entity('sdt', gen1.Sdt) self.configuration.validate() - self.__add_storage_entity('device', objects.Device) - self.__add_storage_entity('fault_set', objects.FaultSet) - self.__add_storage_entity('protection_domain', - objects.ProtectionDomain) - self.__add_storage_entity('sdc', objects.Sdc) - self.__add_storage_entity('sds', objects.Sds) - self.__add_storage_entity('sdt', objects.Sdt) - self.__add_storage_entity('snapshot_policy', objects.SnapshotPolicy) - self.__add_storage_entity('storage_pool', objects.StoragePool) - self.__add_storage_entity('acceleration_pool', - objects.AccelerationPool) - self.__add_storage_entity('system', objects.System) - 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) - self.__add_storage_entity('service_template', objects.ServiceTemplate) - self.__add_storage_entity('managed_device', objects.ManagedDevice) - self.__add_storage_entity('deployment', objects.Deployment) - self.__add_storage_entity( - 'firmware_repository', - objects.FirmwareRepository) - self.__add_storage_entity('host', objects.Host) + utils.init_logger(self.configuration.log_level) if version.parse(self.system.api_version()) < version.Version('3.0'): raise exceptions.PowerFlexClientException( 'PowerFlex (VxFlex OS) versions lower than ' '3.0 are not supported.' ) + + if version.parse(self.system.api_version()) > version.Version('3.0') and \ + version.parse(self.system.api_version()) < version.Version('5.0'): + self.add_objects_gen1() + elif version.parse(self.system.api_version()) > version.Version('5.0'): + self.add_objects_gen2() self.__is_initialized = True + + def add_objects_gen1(self): + self.__add_storage_entity('device', gen1.Device) + self.__add_storage_entity( + 'fault_set', gen1.FaultSet) + self.__add_storage_entity('protection_domain', + gen1.ProtectionDomain) + self.__add_storage_entity('sds', gen1.Sds) + self.__add_storage_entity( + 'snapshot_policy', gen1.SnapshotPolicy) + self.__add_storage_entity('storage_pool', gen1.StoragePool) + self.__add_storage_entity('acceleration_pool', + gen1.AccelerationPool) + self.__add_storage_entity('volume', gen1.Volume) + self.__add_storage_entity('utility', gen1.PowerFlexUtility) + self.__add_storage_entity( + 'replication_consistency_group', + gen1.ReplicationConsistencyGroup) + self.__add_storage_entity('replication_pair', gen1.ReplicationPair) + self.__add_storage_entity('service_template', gen1.ServiceTemplate) + self.__add_storage_entity('managed_device', gen1.ManagedDevice) + self.__add_storage_entity('deployment', gen1.Deployment) + self.__add_storage_entity( + 'firmware_repository', + gen1.FirmwareRepository) + self.__add_storage_entity('host', gen1.Host) + + def add_objects_gen2(self): + self.__add_storage_entity('storage_node', gen2.StorageNode) + # self.__add_storage_entity('volume', gen2.Volume) diff --git a/PyPowerFlex/gen1/objects/__init__.py b/PyPowerFlex/gen1/objects/__init__.py new file mode 100644 index 0000000..31d7e61 --- /dev/null +++ b/PyPowerFlex/gen1/objects/__init__.py @@ -0,0 +1,59 @@ +# 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. + +"""This module contains the objects for interacting with the PowerFlex APIs.""" + +from PyPowerFlex.gen1.objects.device import Device +from PyPowerFlex.gen1.objects.fault_set import FaultSet +from PyPowerFlex.gen1.objects.protection_domain import ProtectionDomain +from PyPowerFlex.gen1.objects.sdc import Sdc +from PyPowerFlex.gen1.objects.sds import Sds +from PyPowerFlex.gen1.objects.sdt import Sdt +from PyPowerFlex.gen1.objects.snapshot_policy import SnapshotPolicy +from PyPowerFlex.gen1.objects.storage_pool import StoragePool +from PyPowerFlex.gen1.objects.acceleration_pool import AccelerationPool +from PyPowerFlex.gen1.objects.system import System +from PyPowerFlex.gen1.objects.volume import Volume +from PyPowerFlex.gen1.objects.utility import PowerFlexUtility +from PyPowerFlex.gen1.objects.replication_consistency_group import ReplicationConsistencyGroup +from PyPowerFlex.gen1.objects.replication_pair import ReplicationPair +from PyPowerFlex.gen1.objects.service_template import ServiceTemplate +from PyPowerFlex.gen1.objects.managed_device import ManagedDevice +from PyPowerFlex.gen1.objects.deployment import Deployment +from PyPowerFlex.gen1.objects.firmware_repository import FirmwareRepository +from PyPowerFlex.gen1.objects.host import Host + + +__all__ = [ + 'Device', + 'FaultSet', + 'ProtectionDomain', + 'Sdc', + 'Sds', + 'Sdt', + 'SnapshotPolicy', + 'StoragePool', + 'AccelerationPool', + 'System', + 'Volume', + 'PowerFlexUtility', + 'ReplicationConsistencyGroup', + 'ReplicationPair', + 'ServiceTemplate', + 'ManagedDevice', + 'Deployment', + 'FirmwareRepository', + 'Host', +] diff --git a/PyPowerFlex/objects/acceleration_pool.py b/PyPowerFlex/gen1/objects/acceleration_pool.py similarity index 100% rename from PyPowerFlex/objects/acceleration_pool.py rename to PyPowerFlex/gen1/objects/acceleration_pool.py diff --git a/PyPowerFlex/objects/deployment.py b/PyPowerFlex/gen1/objects/deployment.py similarity index 100% rename from PyPowerFlex/objects/deployment.py rename to PyPowerFlex/gen1/objects/deployment.py diff --git a/PyPowerFlex/objects/device.py b/PyPowerFlex/gen1/objects/device.py similarity index 100% rename from PyPowerFlex/objects/device.py rename to PyPowerFlex/gen1/objects/device.py diff --git a/PyPowerFlex/objects/fault_set.py b/PyPowerFlex/gen1/objects/fault_set.py similarity index 100% rename from PyPowerFlex/objects/fault_set.py rename to PyPowerFlex/gen1/objects/fault_set.py diff --git a/PyPowerFlex/objects/firmware_repository.py b/PyPowerFlex/gen1/objects/firmware_repository.py similarity index 100% rename from PyPowerFlex/objects/firmware_repository.py rename to PyPowerFlex/gen1/objects/firmware_repository.py diff --git a/PyPowerFlex/objects/host.py b/PyPowerFlex/gen1/objects/host.py similarity index 100% rename from PyPowerFlex/objects/host.py rename to PyPowerFlex/gen1/objects/host.py diff --git a/PyPowerFlex/objects/managed_device.py b/PyPowerFlex/gen1/objects/managed_device.py similarity index 100% rename from PyPowerFlex/objects/managed_device.py rename to PyPowerFlex/gen1/objects/managed_device.py diff --git a/PyPowerFlex/objects/protection_domain.py b/PyPowerFlex/gen1/objects/protection_domain.py similarity index 100% rename from PyPowerFlex/objects/protection_domain.py rename to PyPowerFlex/gen1/objects/protection_domain.py diff --git a/PyPowerFlex/objects/replication_consistency_group.py b/PyPowerFlex/gen1/objects/replication_consistency_group.py similarity index 100% rename from PyPowerFlex/objects/replication_consistency_group.py rename to PyPowerFlex/gen1/objects/replication_consistency_group.py diff --git a/PyPowerFlex/objects/replication_pair.py b/PyPowerFlex/gen1/objects/replication_pair.py similarity index 100% rename from PyPowerFlex/objects/replication_pair.py rename to PyPowerFlex/gen1/objects/replication_pair.py diff --git a/PyPowerFlex/objects/sdc.py b/PyPowerFlex/gen1/objects/sdc.py similarity index 100% rename from PyPowerFlex/objects/sdc.py rename to PyPowerFlex/gen1/objects/sdc.py diff --git a/PyPowerFlex/objects/sds.py b/PyPowerFlex/gen1/objects/sds.py similarity index 100% rename from PyPowerFlex/objects/sds.py rename to PyPowerFlex/gen1/objects/sds.py diff --git a/PyPowerFlex/objects/sdt.py b/PyPowerFlex/gen1/objects/sdt.py similarity index 100% rename from PyPowerFlex/objects/sdt.py rename to PyPowerFlex/gen1/objects/sdt.py diff --git a/PyPowerFlex/objects/service_template.py b/PyPowerFlex/gen1/objects/service_template.py similarity index 100% rename from PyPowerFlex/objects/service_template.py rename to PyPowerFlex/gen1/objects/service_template.py diff --git a/PyPowerFlex/objects/snapshot_policy.py b/PyPowerFlex/gen1/objects/snapshot_policy.py similarity index 100% rename from PyPowerFlex/objects/snapshot_policy.py rename to PyPowerFlex/gen1/objects/snapshot_policy.py diff --git a/PyPowerFlex/objects/storage_pool.py b/PyPowerFlex/gen1/objects/storage_pool.py similarity index 99% rename from PyPowerFlex/objects/storage_pool.py rename to PyPowerFlex/gen1/objects/storage_pool.py index 149b19c..2602e9c 100644 --- a/PyPowerFlex/objects/storage_pool.py +++ b/PyPowerFlex/gen1/objects/storage_pool.py @@ -23,7 +23,7 @@ from PyPowerFlex import base_client from PyPowerFlex import exceptions -from PyPowerFlex.objects import Sds +from PyPowerFlex.gen1.objects import Sds LOG = logging.getLogger(__name__) diff --git a/PyPowerFlex/objects/system.py b/PyPowerFlex/gen1/objects/system.py similarity index 100% rename from PyPowerFlex/objects/system.py rename to PyPowerFlex/gen1/objects/system.py diff --git a/PyPowerFlex/objects/utility.py b/PyPowerFlex/gen1/objects/utility.py similarity index 100% rename from PyPowerFlex/objects/utility.py rename to PyPowerFlex/gen1/objects/utility.py diff --git a/PyPowerFlex/objects/volume.py b/PyPowerFlex/gen1/objects/volume.py similarity index 100% rename from PyPowerFlex/objects/volume.py rename to PyPowerFlex/gen1/objects/volume.py diff --git a/PyPowerFlex/gen2/objects/__init__.py b/PyPowerFlex/gen2/objects/__init__.py new file mode 100644 index 0000000..05d8b69 --- /dev/null +++ b/PyPowerFlex/gen2/objects/__init__.py @@ -0,0 +1,32 @@ +# 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. + +"""This module contains the objects for interacting with the PowerFlex APIs.""" + +from PyPowerFlex.gen2.objects.sdc import Sdc +from PyPowerFlex.gen2.objects.sdt import Sdt +from PyPowerFlex.gen2.objects.storage_node import StorageNode +from PyPowerFlex.gen2.objects.system import System +from PyPowerFlex.gen2.objects.volume import Volume +from PyPowerFlex.gen2.objects.utility import PowerFlexUtility + +__all__ = [ + 'Sdc', + 'Sdt', + 'StorageNode', + 'System', + 'Volume', + 'PowerFlexUtility', +] diff --git a/PyPowerFlex/gen2/objects/sdc.py b/PyPowerFlex/gen2/objects/sdc.py new file mode 100644 index 0000000..614c123 --- /dev/null +++ b/PyPowerFlex/gen2/objects/sdc.py @@ -0,0 +1,94 @@ +# 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 SDC APIs.""" + +import logging +from PyPowerFlex import base_client + + +LOG = logging.getLogger(__name__) + + +class Sdc(base_client.EntityRequest): + """ + A class representing SDC client. + """ + def delete(self, sdc_id): + """Remove PowerFlex SDC. + + :type sdc_id: str + :rtype: None + """ + + return self._delete_entity(sdc_id) + + def get_mapped_volumes(self, sdc_id, filter_fields=None, fields=None): + """Get PowerFlex volumes mapped to SDC. + + :type sdc_id: str + :type filter_fields: dict + :type fields: list|tuple + :rtype: list[dict] + """ + + return self.get_related(sdc_id, 'Volume', filter_fields, fields) + + def rename(self, sdc_id, name): + """Rename PowerFlex SDC. + + :type sdc_id: str + :type name: str + :rtype: dict + """ + + action = 'setSdcName' + + params = {"sdcName": name} + + return self._rename_entity(action, sdc_id, params) + + def set_performance_profile(self, sdc_id, perf_profile): + """Apply a performance profile to the specified SDC. + + :type sdc_id: str + :type perf_profile: str + :rtype: dict + """ + + action = 'setSdcPerformanceParameters' + + params = {"perfProfile": perf_profile} + return self._perform_entity_operation_based_on_action( + sdc_id, action, params=params, add_entity=False) + + def query_selected_statistics(self, properties, ids=None): + """Query PowerFlex SDC statistics. + + :type properties: list + :type ids: list of SDC IDs or None for all SDC + :rtype: dict + """ + + action = "querySelectedStatistics" + + params = {'properties': properties} + + if ids: + params["ids"] = ids + else: + params["allIds"] = "" + + return self._query_selected_statistics(action, params) diff --git a/PyPowerFlex/gen2/objects/sdt.py b/PyPowerFlex/gen2/objects/sdt.py new file mode 100644 index 0000000..40d7878 --- /dev/null +++ b/PyPowerFlex/gen2/objects/sdt.py @@ -0,0 +1,345 @@ +# 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 SDT APIs.""" + +# pylint: disable=too-few-public-methods,no-member,too-many-arguments,too-many-positional-arguments + +import logging +import requests +from PyPowerFlex import base_client +from PyPowerFlex import exceptions +from PyPowerFlex import utils + +LOG = logging.getLogger(__name__) + + +class SdtIp(dict): + """PowerFlex sdt ip object. + + JSON-serializable, should be used as `sdt_ips` list item + in `Sdt.create` method or sdt_ip item in `Sdt.add_sdt_ip` method. + """ + + def __init__(self, ip, role): + params = utils.prepare_params( + { + "ip": ip, + "role": role, + }, + dump=False, + ) + super().__init__(**params) + + +class SdtIpRoles: + """SDT ip roles.""" + + storage_only = "StorageOnly" + host_only = "HostOnly" + storage_and_host = "StorageAndHost" + + +class Sdt(base_client.EntityRequest): + """ + A class representing SDT client. + """ + def create( + self, + sdt_ips, + sdt_name, + protection_domain_id, + storage_port=None, + nvme_port=None, + discovery_port=None, + ): + """Create PowerFlex SDT. + + :type sdt_ips: list[dict] + :type storage_port: int + :type nvme_port: int + :type discovery_port: int + :type sdt_name: str + :type protection_domain_id: str + :rtype: dict + """ + + params = { + "ips": sdt_ips, + "storagePort": storage_port, + "nvmePort": nvme_port, + "discoveryPort": discovery_port, + "name": sdt_name, + "protectionDomainId": protection_domain_id, + } + + return self._create_entity(params) + + def rename(self, sdt_id, name): + """Rename PowerFlex SDT. + + :type sdt_id: str + :type name: str + :rtype: dict + """ + + action = "renameSdt" + + params = {'newName': name} + + return self._rename_entity(action, sdt_id, params) + + def add_ip(self, sdt_id, ip, role): + """Add PowerFlex SDT target IP address. + + :type sdt_id: str + :type ip: str + :type role: str + :rtype: dict + """ + + action = "addIp" + + params = { + "ip": ip, + "role": role, + } + + r, response = self.send_post_request( + self.base_action_url, + action=action, + entity=self.entity, + entity_id=sdt_id, + params=params, + ) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to add IP for PowerFlex {self.entity} " + f"with id {sdt_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=sdt_id) + + def remove_ip(self, sdt_id, ip): + """Remove PowerFlex SDT target IP address. + + :type sdt_id: str + :type ip: str + :rtype: dict + """ + + action = "removeIp" + + params = {"ip": ip} + + r, response = self.send_post_request( + self.base_action_url, + action=action, + entity=self.entity, + entity_id=sdt_id, + params=params, + ) + if r.status_code != requests.codes.ok: + msg = f"Failed to remove IP from PowerFlex {self.entity} " \ + f"with id {sdt_id}. Error: {response}" + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=sdt_id) + + def set_ip_role(self, sdt_id, ip, role): + """Set PowerFlex SDT target IP address role. + + :type sdt_id: str + :type ip: str + :param role: one of predefined attributes of SdtIpRoles + :type role: str + :rtype: dict + """ + + action = "modifyIpRole" + + params = { + "ip": ip, + "newRole": role + } + + r, response = self.send_post_request( + self.base_action_url, + action=action, + entity=self.entity, + entity_id=sdt_id, + params=params, + ) + if r.status_code != requests.codes.ok: + msg = f"Failed to set ip role for PowerFlex {self.entity} " \ + f"with id {sdt_id}. Error: {response}" + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=sdt_id) + + def set_storage_port(self, sdt_id, storage_port): + """Set PowerFlex SDT storage port. + + :type sdt_id: str + :type storage_port: int + :rtype: dict + """ + + action = "modifyStoragePort" + + params = {"newStoragePort": storage_port} + + r, response = self.send_post_request( + self.base_action_url, + action=action, + entity=self.entity, + entity_id=sdt_id, + params=params, + ) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to set storage port for PowerFlex {self.entity} " + f"with id {sdt_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=sdt_id) + + def set_nvme_port(self, sdt_id, nvme_port): + """Set PowerFlex SDT NVMe port. + + :type sdt_id: str + :type nvme_port: int + :rtype: dict + """ + + action = "modifyNvmePort" + + params = {"newNvmePort": nvme_port} + + r, response = self.send_post_request( + self.base_action_url, + action=action, + entity=self.entity, + entity_id=sdt_id, + params=params, + ) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to set nvme port for PowerFlex {self.entity} " + f"with id {sdt_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=sdt_id) + + def set_discovery_port(self, sdt_id, discovery_port): + """Set PowerFlex SDT discovery port. + + :type sdt_id: str + :type discovery_port: int + :rtype: dict + """ + + action = "modifyDiscoveryPort" + + params = {"newDiscoveryPort": discovery_port} + + r, response = self.send_post_request( + self.base_action_url, + action=action, + entity=self.entity, + entity_id=sdt_id, + params=params, + ) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to set discovery port for PowerFlex {self.entity} " + f"with id {sdt_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=sdt_id) + + def enter_maintenance_mode(self, sdt_id): + """Enter Maintenance Mode. + + :type sdt_id: str + :rtype: dict + """ + + action = "enterMaintenanceMode" + + r, response = self.send_post_request( + self.base_action_url, + action=action, + entity=self.entity, + entity_id=sdt_id, + params=None, + ) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to enter maintenance mode for PowerFlex {self.entity} " + f"with id {sdt_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=sdt_id) + + def exit_maintenance_mode(self, sdt_id): + """Exit Maintenance Mode. + + :type sdt_id: str + :rtype: dict + """ + + action = "exitMaintenanceMode" + + r, response = self.send_post_request( + self.base_action_url, + action=action, + entity=self.entity, + entity_id=sdt_id, + params=None, + ) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to exit maintenance mode for PowerFlex {self.entity} " + f"with id {sdt_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=sdt_id) + + def delete(self, sdt_id, force=None): + """Remove PowerFlex SDT. + + :type sdt_id: str + :type force: bool + :rtype: None + """ + + params = {"force": force} + + return self._delete_entity(sdt_id, params) diff --git a/PyPowerFlex/gen2/objects/storage_node.py b/PyPowerFlex/gen2/objects/storage_node.py new file mode 100644 index 0000000..dc3e8f9 --- /dev/null +++ b/PyPowerFlex/gen2/objects/storage_node.py @@ -0,0 +1,244 @@ +# 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 Storage Node APIs.""" + +# pylint: disable=too-few-public-methods,no-member,too-many-arguments,too-many-positional-arguments,too-many-locals,duplicate-code + +import logging + +import requests + +from PyPowerFlex import base_client +from PyPowerFlex import exceptions +from PyPowerFlex import utils + + +LOG = logging.getLogger(__name__) + + + +class StorageNodeIpRoles: + """StorageNode ip roles.""" + + storage = 'Storage' + app = 'App' + storage_and_app = 'StorageAndApp' + + +class StorageNodeIp(dict): + """PowerFlex storage node ip object. + + JSON-serializable, should be used as `ipsList` list item + in `Storage_node.create` method or ipsList item in `Storage_node.add_ip` method. + """ + + def __init__(self, ip, role): + params = utils.prepare_params( + { + 'ip': ip, + 'role': role, + }, + dump=False + ) + super().__init__(**params) + + +class StorageNode(base_client.EntityRequest): + @property + def entity(self): + """ + Returns the entity name. + """ + return "Node" + + """ + A class representing Storage Node client. + """ + def add_ip(self, node_id, node_ip): + """Add PowerFlex Storage Node IP-address. + + :type node_id: str + :type node_ip: dict + :rtype: dict + """ + + action = 'addIp' + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=node_id, + params=node_ip) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to add IP for PowerFlex Storage Node " + f"with id {node_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=node_id) + + def create(self, + name, + node_ips, + protection_domain_id=None, + ): + """Create PowerFlex Storage Node. + :type name: str + :type protection_domain_id: str + :type node_ips: list[dict] + :rtype: dict + """ + + params = { + "protectionDomainId": protection_domain_id, + "ips": node_ips, + "name": name, + } + + return self._create_entity(params) + + def delete(self, node_id, force=None): + """Remove PowerFlex Storage Node. + + :type node_id: str + :type force: bool + :rtype: None + """ + + params = {"force": force} + + return self._delete_entity(node_id, params) + + + def rename(self, node_id, name): + """Rename PowerFlex Storage Node. + + :type node_id: str + :type name: str + :rtype: dict + """ + + action = 'renameStorageNode' + + params = {"name": name} + + return self._rename_entity(action, node_id, params) + + def remove_ip(self, node_id, ip): + """Remove PowerFlex Storage Node IP-address. + + :type node_id: str + :type ip: str + :rtype: dict + """ + + action = 'removeIp' + + params = {"ip": ip} + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=node_id, + params=params) + if r.status_code != requests.codes.ok: + msg = f"Failed to remove IP from PowerFlex Storage Node " \ + f"with id {node_id}. Error: {response}" + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=node_id) + + def set_ip_role(self, node_id, ip, role): + """Set PowerFlex Storage Node IP-address role. + + :type node_id: str + :type ip: str + :param role: one of predefined attributes of StorageNodeIpRoles + :type role: str + :type force: bool + :rtype: dict + """ + + action = 'modifyIpRole' + + params = { + 'ip': ip, + 'newRole': role, + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=node_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to set ip role for PowerFlex Storage Node " + f"with id {node_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=node_id) + + def set_performance_parameters(self, node_id, performance_profile): + """Set performance parameters for PowerFlex Storage Node. + + :type node_id: str + :type performance_profile: str + :rtype: dict + """ + + action = 'setNodePerformanceParameters' + + params = {"perfProfile": performance_profile} + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=node_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to set performance parameters for PowerFlex " + f"Storage Node with id {node_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=node_id) + + # def query_selected_statistics(self, properties, ids=None): + # """Query PowerFlex Storage Node statistics. + + # :type properties: list + # :type ids: list of Storage Node IDs or None for all Storage Node + # :rtype: dict + # """ + + # action = "querySelectedStatistics" + + # params = {'properties': properties} + + # if ids: + # params["ids"] = ids + # else: + # params["allIds"] = "" + + # return self._query_selected_statistics(action, params) diff --git a/PyPowerFlex/gen2/objects/system.py b/PyPowerFlex/gen2/objects/system.py new file mode 100644 index 0000000..40b7957 --- /dev/null +++ b/PyPowerFlex/gen2/objects/system.py @@ -0,0 +1,459 @@ +# 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 + +import logging +import re + +import requests + +from PyPowerFlex import base_client +from PyPowerFlex import exceptions +from PyPowerFlex import utils + + +LOG = logging.getLogger(__name__) + + +class SnapshotDef(dict): + """PowerFlex definition of snapshot to create. + + JSON-serializable, should be used as `snapshot_defs` list item + in `System.snapshot_volumes` method. + """ + + def __init__(self, volume_id, name=None): + """Initialize SnapshotDef object. + + :type volume_id: str + :type name: str + """ + + params = utils.prepare_params( + { + 'volumeId': volume_id, + 'snapshotName': name, + }, + dump=False + ) + super().__init__(**params) + + +class System(base_client.EntityRequest): + """Client for system operations""" + def __init__(self, token, configuration): + self.__api_version = None + super().__init__(token, configuration) + + def api_version(self, cached=True): + """Get PowerFlex API version. + + :param cached: get version from cache or send API response + :type cached: bool + :rtype: str + """ + + url = '/version' + + if not self.__api_version or not cached: + r, response = self.send_get_request(url) + if r.status_code != requests.codes.ok: + exc = exceptions.PowerFlexFailQuerying('API version') + LOG.error(exc.message) + raise exc + pattern = re.compile(r'^\d+(\.\d+)*$') + if not pattern.match(response): + msg = ( + f"Failed to query PowerFlex API version. Invalid version " + f"format: {response}." + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + self.__api_version = response + return self.__api_version + + def remove_cg_snapshots(self, system_id, cg_id, allow_ext_managed=None): + """Remove PowerFlex ConsistencyGroup snapshots. + + :type system_id: str + :type cg_id: str + :type allow_ext_managed: bool + :rtype: dict + """ + + action = 'removeConsistencyGroupSnapshots' + + params = { + "snapGroupId": cg_id, + "allowOnExtManagedVol": allow_ext_managed + } + + 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 remove consistency group snapshots from " + f"PowerFlex {self.entity} with id {system_id}. " + f"Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + 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): + """ + Add a standby MDM to the system. + :param mdm_ips: List of ip addresses assigned to new MDM. It can + contain IPv4 addresses. + :type mdm_ips: list[str] + :param role: Role of the new MDM. + :type role: str + :param management_ips: List of IP addresses used to manage the MDM. + It can contain IPv4 addresses. + :type management_ips: list[str] + :param port: Port of new MDM. Default: 9011 + :type port: str + :param mdm_name: Name of the new MDM. + :type mdm_name: str + :param allow_multiple_ips: Allow the added node to have a different + number of IPs from the primary node. + :type allow_multiple_ips: str + :param clean: Clean a previous MDM configuration. + :type clean: str + :param virtual_interface: List of NIC interfaces that will be used + for virtual IP address. + :type virtual_interface: list[str] + :return: ID of new standby MDM. + :rtype: dict + """ + action = 'addStandbyMdm' + params = { + "ips": mdm_ips, + "role": role, + "managementIps": management_ips, + "name": mdm_name, + "port": port, + "allowAsymmetricIps": allow_multiple_ips, + "forceClean": clean, + "virtIpIntfs": virtual_interface + } + + r, response = self.send_post_request(self.base_object_url, + action=action, + entity=self.entity, + params=params) + if r.status_code != requests.codes.ok: + msg = f"Failed to add standBy MDM on PowerFlex {self.entity}. " \ + f"Error: {response}" + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response + + def remove_standby_mdm(self, mdm_id): + """ + Remove a standby MDM from the system. + :param mdm_id: ID of MDM to be removed. + :type mdm_id: str + :return: None + """ + action = 'removeStandbyMdm' + params = { + "id": mdm_id + } + + r, response = self.send_mdm_cluster_post_request(self.base_object_url, + action=action, + entity=self.entity, + params=params) + if r.status_code != requests.codes.ok and response is not None: + msg = ( + f"Failed to remove standBy MDM from PowerFlex {self.entity}. " + f"Error: {response}." + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return True + + def get_mdm_cluster_details(self): + """ + Get the MDM cluster details + :return: MDM cluster details + :rtype: dict + """ + + r, response = self.send_post_request(self.query_mdm_cluster_url, + entity=self.entity) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to get MDM cluster details on PowerFlex {self.entity}. " + f"Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response + + def get_gateway_configuration_details(self): + """ + Get the gateway configuration details + :return: Gateway configuration details + :rtype: dict + """ + + r, response = self.send_get_request('/Configuration') + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to get gateway configuration details on PowerFlex {self.entity}. " + f"Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response + + def change_mdm_ownership(self, mdm_id): + """ + Change MDM cluster ownership from current master MDM to different MDM. + + :param mdm_id: ID of New Manager MDM + :type mdm_id: str + :return: None + :rtype: dict + """ + action = 'changeMdmOwnership' + params = { + "id": mdm_id + } + + r, response = self.send_post_request(self.base_object_url, + action=action, + entity=self.entity, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to change ownership on PowerFlex {self.entity}. " + f"Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def set_cluster_mdm_performance_profile(self, performance_profile): + """ + Set the Cluster MDMs performance profile. + + :param performance_profile: Define the performance profile of MDMs. + :type performance_profile: str + :return: None + :rtype: dict + """ + action = 'setMdmPerformanceParameters' + params = {"perfProfile": performance_profile} + + r, response = self.send_post_request(self.base_object_url, + action=action, + entity=self.entity, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to set performance profile of MDMs on PowerFlex " + f"{self.entity}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response + + def rename_mdm(self, mdm_id, mdm_new_name): + """ + Set the Cluster MDMs performance profile. + + :param mdm_id: ID of MDM. + :type mdm_id: str + :param mdm_new_name: new name of MDM. + :type mdm_new_name: str + :return: None + :rtype: dict + """ + action = 'renameMdm' + params = { + "id": mdm_id, + "newName": mdm_new_name + } + + r, response = self.send_post_request(self.base_object_url, + action=action, + entity=self.entity, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to rename the MDM on PowerFlex {self.entity}. " + f"Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response + + def modify_virtual_ip_interface(self, mdm_id, virtual_ip_interfaces=None, + clear_interfaces=None): + """ + Set the Cluster MDMs performance profile. + + :param mdm_id: ID of MDM. + :type mdm_id: str + :param virtual_ip_interfaces: List of interface names to be used for + the MDM virtual IPs. + :type virtual_ip_interfaces: list[str] + :param clear_interfaces: Clear all virtual IP interfaces. + :type mdm_id: str + :return: None + :rtype: dict + """ + action = 'modifyVirtualIpInterfaces' + if virtual_ip_interfaces is not None: + params = { + "id": mdm_id, + "virtIpIntfs": virtual_ip_interfaces + } + else: + params = { + "id": mdm_id, + "clear": clear_interfaces + } + + r, response = self.send_post_request(self.base_object_url, + action=action, + entity=self.entity, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to modify virtual IP interface on PowerFlex " + f"{self.entity}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response + + def switch_cluster_mode( + self, + cluster_mode, + add_secondary=None, + remove_secondary=None, + add_tb=None, + remove_tb=None): + """ + Switch cluster mode. + + :param cluster_mode: New mode of MDM cluster + :type cluster_mode: str + :param add_secondary: List of secondary MDM IDs that will be part of + the cluster. A maximum of two IDs are allowed. + :type add_secondary: list[str] + :param remove_secondary: List of secondary MDM IDs that will be removed + from the cluster. + :type remove_secondary: list[str] + :param add_tb: List of TieBreaker MDM IDs that will be part of the + cluster. + :type add_tb: list[str] + :param remove_tb: List of TieBreaker MDM IDs that will be removed + from the cluster. + :type remove_tb: list[str] + :return: None + """ + action = 'switchClusterMode' + params = { + "mode": cluster_mode, + "addSlaveMdmIdList": add_secondary, + "addTBIdList": add_tb, + "removeSlaveMdmIdList": remove_secondary, + "removeTBIdList": remove_tb + } + + r, response = self.send_mdm_cluster_post_request(self.base_object_url, + action=action, + entity=self.entity, + params=params) + if r.status_code != requests.codes.ok and response is not None: + msg = f"Failed to switch MDM cluster mode PowerFlex {self.entity}. " \ + f"Error: {response}." + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return True + + def query_selected_statistics(self, properties): + """Query PowerFlex system statistics. + + :type properties: list + :rtype: dict + """ + + action = "querySelectedStatistics" + + params = {'properties': properties} + + return self._query_selected_statistics(action, params) diff --git a/PyPowerFlex/gen2/objects/utility.py b/PyPowerFlex/gen2/objects/utility.py new file mode 100644 index 0000000..db60eea --- /dev/null +++ b/PyPowerFlex/gen2/objects/utility.py @@ -0,0 +1,140 @@ +# 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. + +"""Utility module for PowerFlex.""" + +# pylint: disable=no-member,useless-parent-delegation +import logging + +import requests + +from PyPowerFlex import base_client +from PyPowerFlex import exceptions +from PyPowerFlex.constants import StoragePoolConstants, VolumeConstants, SnapshotPolicyConstants + + +LOG = logging.getLogger(__name__) + + +class PowerFlexUtility(base_client.EntityRequest): + "Utility class for PowerFlex" + def __init__(self, token, configuration): + super().__init__(token, configuration) + + def get_statistics_for_all_storagepools(self, ids=None, properties=None): + """list storagepool statistics for PowerFlex. + + :param ids: list + :param properties: list + :return: dict + """ + + action = 'querySelectedStatistics' + version = self.get_api_version() + default_properties = StoragePoolConstants.DEFAULT_STATISTICS_PROPERTIES + if version != '3.5': + default_properties = default_properties + \ + StoragePoolConstants.DEFAULT_STATISTICS_PROPERTIES_ABOVE_3_5 + params = { + 'properties': default_properties if properties is None else properties} + if ids is None: + params['allIds'] = "" + else: + params['ids'] = ids + + r, response = self.send_post_request(self.list_statistics_url, + entity='StoragePool', + action=action, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to list storage pool statistics for PowerFlex. " + f"Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response + + def get_statistics_for_all_volumes(self, ids=None, properties=None): + """list volume statistics for PowerFlex. + + :param ids: list + :param properties: list + :return: dict + """ + + action = 'querySelectedStatistics' + + params = { + 'properties': ( + VolumeConstants.DEFAULT_STATISTICS_PROPERTIES + if properties is None + else properties + ) + } + if ids is None: + params['allIds'] = "" + else: + params['ids'] = ids + + r, response = self.send_post_request(self.list_statistics_url, + entity='Volume', + action=action, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + 'Failed to list volume statistics for PowerFlex. ' + f'Error: {response}' + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response + + def get_statistics_for_all_snapshot_policies( + self, ids=None, properties=None): + """list snapshot policy statistics for PowerFlex. + + :param ids: list + :param properties: list + :return: dict + """ + + action = 'querySelectedStatistics' + + params = {} + if properties is None: + params['properties'] = SnapshotPolicyConstants.DEFAULT_STATISTICS_PROPERTIES + else: + params['properties'] = properties + if ids is None: + params['allIds'] = "" + else: + params['ids'] = ids + + r, response = self.send_post_request(self.list_statistics_url, + entity='SnapshotPolicy', + action=action, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to list snapshot policy statistics for PowerFlex. " + f"Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response diff --git a/PyPowerFlex/gen2/objects/volume.py b/PyPowerFlex/gen2/objects/volume.py new file mode 100644 index 0000000..c160f19 --- /dev/null +++ b/PyPowerFlex/gen2/objects/volume.py @@ -0,0 +1,574 @@ +# 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.""" + +# 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 + +LOG = logging.getLogger(__name__) + + +class CompressionMethod: + """Volume compression methods.""" + + invalid = 'Invalid' + none = 'None' + normal = 'Normal' + + +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', 'management', 'snap_mobility', + 'ntnx']) + for vol_class in supported_vol_classes: + locals()[vol_class] = vol_class + + +class Volume(base_client.EntityRequest): + """ + A class representing Volume client. + """ + def add_mapped_sdc(self, + volume_id, + sdc_id=None, + sdc_guid=None, + allow_multiple_mappings=None, + allow_ext_managed=None, + access_mode=None, + volume_class=VolumeClass.defaultclass): + """Map PowerFlex volume to SDC. + + :param volume_id: str + :param sdc_id: str + :param sdc_guid: str + :param allow_multiple_mappings: bool + :param allow_ext_managed: bool + :type access_mode: str + :param volume_class: str + :return: dict + """ + + action = 'addMappedSdc' + + if all([sdc_id, sdc_guid]) or not any([sdc_id, sdc_guid]): + msg = 'Either sdc_id or sdc_guid must be set.' + raise exceptions.InvalidInput(msg) + params = { + "sdcId": sdc_id, + "guid": sdc_guid, + "allowMultipleMappings": allow_multiple_mappings, + "allowOnExtManagedVol": allow_ext_managed, + "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 SDC. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=volume_id) + + def create(self, + storage_pool_id, + size_in_gb, + name=None, + volume_type=None, + use_rmcache=None, + compression_method=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 compression_method: one of predefined attributes of + CompressionMethod + :type compression_method: str + :param volume_class: str + :return: dict + """ + + params = { + 'storagePoolId': storage_pool_id, + 'volumeSizeInGb': size_in_gb, + 'name': name, + 'volumeType': volume_type, + 'useRmcache': use_rmcache, + 'compressionMethod': compression_method, + 'volumeClass': volume_class + } + + return self._create_entity(params) + + def delete(self, volume_id, remove_mode, allow_ext_managed=None, + volume_class=VolumeClass.defaultclass): + """Remove PowerFlex volume. + + :param volume_id: str + :param remove_mode: one of predefined attributes of RemoveMode + :param allow_ext_managed: bool + :param volume_class: str + :return: None + """ + + params = { + "removeMode": remove_mode, + "allowOnExtManagedVol": allow_ext_managed, + "volumeClass": volume_class + } + + return self._delete_entity(volume_id, params) + + def extend(self, volume_id, size_in_gb, allow_ext_managed=None, + volume_class=VolumeClass.defaultclass): + """Extend PowerFlex volume. + + :param volume_id: str + :param size_in_gb: int + :param allow_ext_managed: bool + :param volume_class: str + :return: dict + """ + + action = 'setVolumeSize' + + params = {"sizeInGB": size_in_gb, + "allowOnExtManagedVol": allow_ext_managed, + "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) + + return self.get(entity_id=volume_id) + + def get_statistics(self, volume_id, fields=None): + """Get related PowerFlex Statistics for volume. + + :type volume_id: str + :type fields: list|tuple + :rtype: dict + """ + + return self.get_related(volume_id, + 'Statistics', + fields) + + def lock_auto_snapshot(self, volume_id): + """Lock auto snapshot of PowerFlex volume. + + :param volume_id: str + :return: dict + """ + + action = 'lockAutoSnapshot' + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=volume_id) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to lock AutoSnapshot for PowerFlex {self.entity} " + f"with id {volume_id}. " + f"Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=volume_id) + + def remove_mapped_sdc(self, + volume_id, + sdc_id=None, + sdc_guid=None, + all_sdcs=None, + skip_appliance_validation=None, + allow_ext_managed=None, + volume_class=VolumeClass.defaultclass): + """Unmap PowerFlex volume from SDC. + + :param volume_id: str + :param sdc_id: str + :param sdc_guid: str + :param all_sdcs: bool + :param skip_appliance_validation: bool + :param allow_ext_managed: bool + :param volume_class: str + :return: dict + """ + + action = 'removeMappedSdc' + + if ( + all([sdc_id, sdc_guid, all_sdcs]) or + not any([sdc_id, sdc_guid, all_sdcs]) + ): + msg = 'Either sdc_id or sdc_guid or all_sdcs must be set.' + raise exceptions.InvalidInput(msg) + + params = { + "sdcId": sdc_id, + "guid": sdc_guid, + "allSdcs": all_sdcs, + "skipApplianceValidation": skip_appliance_validation, + "allowOnExtManagedVol": allow_ext_managed, + "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"SDC. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=volume_id) + + def rename(self, volume_id, name, allow_ext_managed=None, + volume_class=VolumeClass.defaultclass): + """Rename PowerFlex volume. + + :param volume_id: str + :param name: str + :param allow_ext_managed: bool + :param volume_class: str + :return: dict + """ + + action = 'setVolumeName' + + params = { + "newName": name, + "allowOnExtManagedVol": allow_ext_managed, + "volumeClass": volume_class + } + + return self._rename_entity(action, volume_id, params) + + def unlock_auto_snapshot(self, volume_id, remove_auto_snapshot=None): + """Unlock auto snapshot of PowerFlex volume. + + :param volume_id: str + :param remove_auto_snapshot: bool + :return: dict + """ + + action = 'unlockAutoSnapshot' + + params = { + "autoSnapshotWillBeRemoved": remove_auto_snapshot + } + + 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 unlock AutoSnapshot for PowerFlex {self.entity} " + f"with id {volume_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=volume_id) + + 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) + + return self.get(entity_id=volume_id) + + def set_compression_method(self, volume_id, compression_method): + """ + Modify the compression method to be used for a Volume, relevant only + if the volume has a space efficient data layout. + + :param volume_id: ID of the volume + :type volume_id: str + :param compression_method: one of predefined attributes of + CompressionMethod + :type compression_method: str + :return: dict + """ + + action = 'modifyCompressionMethod' + + params = { + 'compressionMethod': compression_method, + } + + 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 compression method of PowerFlex " + f"{self.entity} with id {volume_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=volume_id) + + def set_use_rmcache(self, volume_id, use_rmcache): + """ + Control the use of Read RAM Cache in the specified volume. + If you want to ensure that all I/O operations for this volume are + cached, the relevant Storage Pool should be configured to use cache, + and the relevant SDSs should all have caching enabled. + + :param volume_id: ID of the volume + :type volume_id: str + :param use_rmcache: Whether to use Read RAM cache or not + :type use_rm_cache: bool + :return: dict + """ + + action = 'setVolumeUseRmcache' + + params = { + "useRmcache": use_rmcache + } + + 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 use_rmcache of PowerFlex " + f"{self.entity} with id {volume_id}. " + f"Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=volume_id) + + 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) + + return self.get(entity_id=volume_id) + + 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) + + return self.get(entity_id=snap_id) + + def set_volume_access_mode_limit(self, volume_id, access_mode_limit): + """ + Set the highest mapping access mode allowed for a volume. + + :param volume_id: ID of the volume + :type volume_id: str + :param access_mode_limit: Define the access mode limit of a volume, + options are ReadWrite or ReadOnly + :type access_mode_limit: str + :return: dict + """ + + action = 'setVolumeAccessModeLimit' + + params = {"accessModeLimit": access_mode_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 Volume Access Mode Limit of PowerFlex " + f"{self.entity} with id {volume_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=volume_id) + + def query_selected_statistics(self, properties, ids=None): + """Query PowerFlex volume statistics. + + :type properties: list + :type ids: list of volume IDs or None for all volumes + :rtype: dict + """ + + action = "querySelectedStatistics" + + params = {'properties': properties} + + if ids: + params["ids"] = ids + else: + params["allIds"] = "" + + return self._query_selected_statistics(action, params) diff --git a/PyPowerFlex/objects/__init__.py b/PyPowerFlex/objects/__init__.py deleted file mode 100644 index 716fb43..0000000 --- a/PyPowerFlex/objects/__init__.py +++ /dev/null @@ -1,59 +0,0 @@ -# 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. - -"""This module contains the objects for interacting with the PowerFlex APIs.""" - -from PyPowerFlex.objects.device import Device -from PyPowerFlex.objects.fault_set import FaultSet -from PyPowerFlex.objects.protection_domain import ProtectionDomain -from PyPowerFlex.objects.sdc import Sdc -from PyPowerFlex.objects.sds import Sds -from PyPowerFlex.objects.sdt import Sdt -from PyPowerFlex.objects.snapshot_policy import SnapshotPolicy -from PyPowerFlex.objects.storage_pool import StoragePool -from PyPowerFlex.objects.acceleration_pool import AccelerationPool -from PyPowerFlex.objects.system import System -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 -from PyPowerFlex.objects.service_template import ServiceTemplate -from PyPowerFlex.objects.managed_device import ManagedDevice -from PyPowerFlex.objects.deployment import Deployment -from PyPowerFlex.objects.firmware_repository import FirmwareRepository -from PyPowerFlex.objects.host import Host - - -__all__ = [ - 'Device', - 'FaultSet', - 'ProtectionDomain', - 'Sdc', - 'Sds', - 'Sdt', - 'SnapshotPolicy', - 'StoragePool', - 'AccelerationPool', - 'System', - 'Volume', - 'PowerFlexUtility', - 'ReplicationConsistencyGroup', - 'ReplicationPair', - 'ServiceTemplate', - 'ManagedDevice', - 'Deployment', - 'FirmwareRepository', - 'Host', -] diff --git a/setup.py b/setup.py index bf8b340..7a91809 100644 --- a/setup.py +++ b/setup.py @@ -17,11 +17,11 @@ # pylint: disable=import-error -from setuptools import setup +from setuptools import setup, find_packages setup( name='PyPowerFlex', - version='1.14.1', + version='1.15.0', description='Python library for Dell PowerFlex', author='Ansible Team at Dell', author_email='ansible.team@dell.com', @@ -33,7 +33,8 @@ classifiers=['License :: OSI Approved :: Apache Software License'], packages=[ 'PyPowerFlex', - 'PyPowerFlex.objects', + 'PyPowerFlex.gen1', + 'PyPowerFlex.gen2', ], python_requires='>=3.5' ) diff --git a/tests/__init__.py b/tests/__init__.py index 74cebf5..e69de29 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,233 +0,0 @@ -# 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. - -"""This module is used for the initialization of the test framework.""" - -# pylint: disable=too-many-instance-attributes,keyword-arg-before-vararg,broad-exception-raised,unused-argument - -import collections -import contextlib -import json -import logging -from unittest import mock -from unittest import TestCase - -import requests - -import PyPowerFlex -from PyPowerFlex import utils - - -class MockResponse(requests.Response): - """ - Mock HTTP Response. - - Defines http replies from mocked calls to do_request(). - """ - def __init__(self, content, status_code=200): - """ - Initialize a MockResponse. - - Args: - content (str or dict): The content of the response. - status_code (int): The status code of the response. - """ - super().__init__() - self._content = content - self.request = mock.MagicMock() - self.status_code = status_code - - def json(self, **kwargs): - """ - Return the content of the response as JSON. - - Args: - **kwargs: Additional keyword arguments. - - Returns: - dict: The content of the response. - """ - return self._content - - @property - def text(self): - """ - Return the content of the response as text. - - Returns: - str: The content of the response. - """ - if not isinstance(self._content, bytes): - return json.dumps(self._content) - return super().text - - -class PyPowerFlexTestCase(TestCase): - """ - Base test case for PyPowerFlex. - - Provides a mocked HTTP response for testing. - """ - RESPONSE_MODE = ( - collections.namedtuple('RESPONSE_MODE', 'Valid Invalid BadStatus') - (Valid='Valid', Invalid='Invalid', BadStatus='BadStatus') - ) - BAD_STATUS_RESPONSE = MockResponse( - { - 'errorCode': 500, - 'message': 'Test default bad status', - }, 500 - ) - MOCK_RESPONSES = {} - DEFAULT_MOCK_RESPONSES = { - RESPONSE_MODE.Valid: { - '/login': 'token', - '/version': '3.5', - '/logout': '', - }, - RESPONSE_MODE.Invalid: { - '/version': '2.5', - }, - RESPONSE_MODE.BadStatus: { - '/login': MockResponse( - { - 'errorCode': 1, - 'message': 'Test login bad status', - }, 400 - ), - '/version': MockResponse( - { - 'errorCode': 2, - 'message': 'Test version bad status', - }, 400 - ), - '/logout': MockResponse( - { - 'errorCode': 3, - 'message': 'Test logout bad status', - }, 400 - ) - } - } - __http_response_mode = RESPONSE_MODE.Valid - - def setUp(self): - """ - Set up the test case. - - Creates a PyPowerFlex client and sets up mocked HTTP responses. - """ - self.gateway_address = '1.2.3.4' - self.gateway_port = 443 - self.username = 'admin' - self.password = 'admin' - self.client = PyPowerFlex.PowerFlexClient(self.gateway_address, - self.gateway_port, - self.username, - self.password, - log_level=logging.DEBUG) - requests.request = self.get_mock_response - self.get_mock = self.mock_object(requests, - 'get', - side_effect=self.get_mock_response) - self.post_mock = self.mock_object(requests, - 'post', - side_effect=self.get_mock_response) - utils.is_version_3 = mock.MagicMock(return_value=True) - - def mock_object(self, obj, attr_name, *args, **kwargs): - """Use python mock to mock an object attribute. - - Mocks the specified objects attribute with the given value. - Automatically performs 'addCleanup' for the mock. - """ - patcher = mock.patch.object(obj, attr_name, *args, **kwargs) - result = patcher.start() - self.addCleanup(patcher.stop) - return result - - @contextlib.contextmanager - def http_response_mode(self, mode): - """ - Context manager for setting the HTTP response mode. - - Args: - mode: The HTTP response mode. - - Yields: - None. - """ - previous_response_mode, self.__http_response_mode = ( - self.__http_response_mode, mode - ) - yield - self.__http_response_mode = previous_response_mode - - def get_mock_response(self, url, request_url=None, mode=None, *args, **kwargs): - """ - Get a mock HTTP response. - - Args: - url (str): The URL of the request. - request_url (str): The URL of the request. - mode (str): The HTTP response mode. - *args: Additional arguments. - **kwargs: Additional keyword arguments. - - Returns: - requests.Response: The mocked HTTP response. - """ - if mode is None: - mode = self.__http_response_mode - - api_path = url.split('/api')[1] if ('/api' in url) else request_url.split('/api')[1] - try: - if api_path == "/login": - response = self.RESPONSE_MODE.Valid[0] - elif api_path == "/logout": - response = self.RESPONSE_MODE.Valid[2] - else: - response = self.MOCK_RESPONSES[mode][api_path] - except KeyError as e: - try: - response = self.DEFAULT_MOCK_RESPONSES[mode][api_path] - except KeyError: - if mode == self.RESPONSE_MODE.BadStatus: - response = self.BAD_STATUS_RESPONSE - else: - raise Exception( - f"Mock API Endpoint is not implemented: [{mode}]" - f"{api_path}" - ) from e - if not isinstance(response, MockResponse): - response = self._get_mock_response(response) - - response.request.url = url - response.request.body = kwargs.get('data') - return response - - def _get_mock_response(self, response): - """ - Returns a MockResponse object based on the given response. - - Args: - response (str): The response to be wrapped. - - Returns: - MockResponse: The mock response object. - """ - if "204" in str(response): - return MockResponse(response, 204) - return MockResponse(response, 200) diff --git a/tests/gen1/__init__.py b/tests/gen1/__init__.py new file mode 100644 index 0000000..74cebf5 --- /dev/null +++ b/tests/gen1/__init__.py @@ -0,0 +1,233 @@ +# 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. + +"""This module is used for the initialization of the test framework.""" + +# pylint: disable=too-many-instance-attributes,keyword-arg-before-vararg,broad-exception-raised,unused-argument + +import collections +import contextlib +import json +import logging +from unittest import mock +from unittest import TestCase + +import requests + +import PyPowerFlex +from PyPowerFlex import utils + + +class MockResponse(requests.Response): + """ + Mock HTTP Response. + + Defines http replies from mocked calls to do_request(). + """ + def __init__(self, content, status_code=200): + """ + Initialize a MockResponse. + + Args: + content (str or dict): The content of the response. + status_code (int): The status code of the response. + """ + super().__init__() + self._content = content + self.request = mock.MagicMock() + self.status_code = status_code + + def json(self, **kwargs): + """ + Return the content of the response as JSON. + + Args: + **kwargs: Additional keyword arguments. + + Returns: + dict: The content of the response. + """ + return self._content + + @property + def text(self): + """ + Return the content of the response as text. + + Returns: + str: The content of the response. + """ + if not isinstance(self._content, bytes): + return json.dumps(self._content) + return super().text + + +class PyPowerFlexTestCase(TestCase): + """ + Base test case for PyPowerFlex. + + Provides a mocked HTTP response for testing. + """ + RESPONSE_MODE = ( + collections.namedtuple('RESPONSE_MODE', 'Valid Invalid BadStatus') + (Valid='Valid', Invalid='Invalid', BadStatus='BadStatus') + ) + BAD_STATUS_RESPONSE = MockResponse( + { + 'errorCode': 500, + 'message': 'Test default bad status', + }, 500 + ) + MOCK_RESPONSES = {} + DEFAULT_MOCK_RESPONSES = { + RESPONSE_MODE.Valid: { + '/login': 'token', + '/version': '3.5', + '/logout': '', + }, + RESPONSE_MODE.Invalid: { + '/version': '2.5', + }, + RESPONSE_MODE.BadStatus: { + '/login': MockResponse( + { + 'errorCode': 1, + 'message': 'Test login bad status', + }, 400 + ), + '/version': MockResponse( + { + 'errorCode': 2, + 'message': 'Test version bad status', + }, 400 + ), + '/logout': MockResponse( + { + 'errorCode': 3, + 'message': 'Test logout bad status', + }, 400 + ) + } + } + __http_response_mode = RESPONSE_MODE.Valid + + def setUp(self): + """ + Set up the test case. + + Creates a PyPowerFlex client and sets up mocked HTTP responses. + """ + self.gateway_address = '1.2.3.4' + self.gateway_port = 443 + self.username = 'admin' + self.password = 'admin' + self.client = PyPowerFlex.PowerFlexClient(self.gateway_address, + self.gateway_port, + self.username, + self.password, + log_level=logging.DEBUG) + requests.request = self.get_mock_response + self.get_mock = self.mock_object(requests, + 'get', + side_effect=self.get_mock_response) + self.post_mock = self.mock_object(requests, + 'post', + side_effect=self.get_mock_response) + utils.is_version_3 = mock.MagicMock(return_value=True) + + def mock_object(self, obj, attr_name, *args, **kwargs): + """Use python mock to mock an object attribute. + + Mocks the specified objects attribute with the given value. + Automatically performs 'addCleanup' for the mock. + """ + patcher = mock.patch.object(obj, attr_name, *args, **kwargs) + result = patcher.start() + self.addCleanup(patcher.stop) + return result + + @contextlib.contextmanager + def http_response_mode(self, mode): + """ + Context manager for setting the HTTP response mode. + + Args: + mode: The HTTP response mode. + + Yields: + None. + """ + previous_response_mode, self.__http_response_mode = ( + self.__http_response_mode, mode + ) + yield + self.__http_response_mode = previous_response_mode + + def get_mock_response(self, url, request_url=None, mode=None, *args, **kwargs): + """ + Get a mock HTTP response. + + Args: + url (str): The URL of the request. + request_url (str): The URL of the request. + mode (str): The HTTP response mode. + *args: Additional arguments. + **kwargs: Additional keyword arguments. + + Returns: + requests.Response: The mocked HTTP response. + """ + if mode is None: + mode = self.__http_response_mode + + api_path = url.split('/api')[1] if ('/api' in url) else request_url.split('/api')[1] + try: + if api_path == "/login": + response = self.RESPONSE_MODE.Valid[0] + elif api_path == "/logout": + response = self.RESPONSE_MODE.Valid[2] + else: + response = self.MOCK_RESPONSES[mode][api_path] + except KeyError as e: + try: + response = self.DEFAULT_MOCK_RESPONSES[mode][api_path] + except KeyError: + if mode == self.RESPONSE_MODE.BadStatus: + response = self.BAD_STATUS_RESPONSE + else: + raise Exception( + f"Mock API Endpoint is not implemented: [{mode}]" + f"{api_path}" + ) from e + if not isinstance(response, MockResponse): + response = self._get_mock_response(response) + + response.request.url = url + response.request.body = kwargs.get('data') + return response + + def _get_mock_response(self, response): + """ + Returns a MockResponse object based on the given response. + + Args: + response (str): The response to be wrapped. + + Returns: + MockResponse: The mock response object. + """ + if "204" in str(response): + return MockResponse(response, 204) + return MockResponse(response, 200) diff --git a/tests/test_acceleration_pool.py b/tests/gen1/test_acceleration_pool.py similarity index 95% rename from tests/test_acceleration_pool.py rename to tests/gen1/test_acceleration_pool.py index 195bfb9..0d253fc 100644 --- a/tests/test_acceleration_pool.py +++ b/tests/gen1/test_acceleration_pool.py @@ -13,16 +13,16 @@ # License for the specific language governing permissions and limitations # under the License. -"""Module for testing accelaration pool client.""" +"""Module for testing acceleration pool client.""" # pylint: disable=invalid-name from PyPowerFlex import exceptions -from PyPowerFlex.objects import acceleration_pool -import tests +from PyPowerFlex.gen1.objects import acceleration_pool +from tests.gen1 import PyPowerFlexTestCase -class TestAccelerationPoolClient(tests.PyPowerFlexTestCase): +class TestAccelerationPoolClient(PyPowerFlexTestCase): """ Test class for the AccelerationPoolClient. """ diff --git a/tests/test_base.py b/tests/gen1/test_base.py similarity index 98% rename from tests/test_base.py rename to tests/gen1/test_base.py index 1068f8b..e3a54cd 100644 --- a/tests/test_base.py +++ b/tests/gen1/test_base.py @@ -19,10 +19,10 @@ from PyPowerFlex import exceptions from PyPowerFlex import utils -import tests +from tests.gen1 import PyPowerFlexTestCase -class TestBaseClient(tests.PyPowerFlexTestCase): +class TestBaseClient(PyPowerFlexTestCase): """ Test class for the BaseClient. """ diff --git a/tests/test_deployment.py b/tests/gen1/test_deployment.py similarity index 98% rename from tests/test_deployment.py rename to tests/gen1/test_deployment.py index 57d26e6..2dfb75d 100644 --- a/tests/test_deployment.py +++ b/tests/gen1/test_deployment.py @@ -18,10 +18,9 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -import tests +from tests.gen1 import PyPowerFlexTestCase - -class TestDeploymentClient(tests.PyPowerFlexTestCase): +class TestDeploymentClient(PyPowerFlexTestCase): """ Test class for the DeploymentClient. """ diff --git a/tests/test_device.py b/tests/gen1/test_device.py similarity index 97% rename from tests/test_device.py rename to tests/gen1/test_device.py index 9c604ac..d80a327 100644 --- a/tests/test_device.py +++ b/tests/gen1/test_device.py @@ -18,11 +18,11 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -from PyPowerFlex.objects.device import MediaType -import tests +from PyPowerFlex.gen1.objects.device import MediaType +from tests.gen1 import PyPowerFlexTestCase -class TestDeviceClient(tests.PyPowerFlexTestCase): +class TestDeviceClient(PyPowerFlexTestCase): """ Test class for DeviceClient. """ diff --git a/tests/test_fault_set.py b/tests/gen1/test_fault_set.py similarity index 98% rename from tests/test_fault_set.py rename to tests/gen1/test_fault_set.py index 770f064..89b215e 100644 --- a/tests/test_fault_set.py +++ b/tests/gen1/test_fault_set.py @@ -18,10 +18,10 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -import tests +from tests.gen1 import PyPowerFlexTestCase -class TestFaultSetClient(tests.PyPowerFlexTestCase): +class TestFaultSetClient(PyPowerFlexTestCase): """ Test class for the PowerFlex FaultSetClient. """ diff --git a/tests/test_firmware_repository.py b/tests/gen1/test_firmware_repository.py similarity index 95% rename from tests/test_firmware_repository.py rename to tests/gen1/test_firmware_repository.py index 7ce7112..475adba 100644 --- a/tests/test_firmware_repository.py +++ b/tests/gen1/test_firmware_repository.py @@ -18,10 +18,10 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -import tests +from tests.gen1 import PyPowerFlexTestCase -class TestFirmwareRepositoryClient(tests.PyPowerFlexTestCase): +class TestFirmwareRepositoryClient(PyPowerFlexTestCase): """ Test class for FirmwareRepositoryClient. """ diff --git a/tests/test_host.py b/tests/gen1/test_host.py similarity index 97% rename from tests/test_host.py rename to tests/gen1/test_host.py index 4012d2d..7c9ecaf 100644 --- a/tests/test_host.py +++ b/tests/gen1/test_host.py @@ -18,10 +18,10 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -import tests +from tests.gen1 import PyPowerFlexTestCase -class TestHostClient(tests.PyPowerFlexTestCase): +class TestHostClient(PyPowerFlexTestCase): """ Tests for the HostClient class. """ diff --git a/tests/test_managed_device.py b/tests/gen1/test_managed_device.py similarity index 95% rename from tests/test_managed_device.py rename to tests/gen1/test_managed_device.py index 9e5039c..5a724a5 100644 --- a/tests/test_managed_device.py +++ b/tests/gen1/test_managed_device.py @@ -18,10 +18,10 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -import tests +from tests.gen1 import PyPowerFlexTestCase -class TestManagedDeviceClient(tests.PyPowerFlexTestCase): +class TestManagedDeviceClient(PyPowerFlexTestCase): """ Test class for the ManagedDeviceClient. """ diff --git a/tests/test_protection_domain.py b/tests/gen1/test_protection_domain.py similarity index 98% rename from tests/test_protection_domain.py rename to tests/gen1/test_protection_domain.py index 8d3146a..74fef7d 100644 --- a/tests/test_protection_domain.py +++ b/tests/gen1/test_protection_domain.py @@ -18,11 +18,11 @@ # pylint: disable=invalid-name,too-many-public-methods,duplicate-code from PyPowerFlex import exceptions -from PyPowerFlex.objects import protection_domain -import tests +from PyPowerFlex.gen1.objects import protection_domain +from tests.gen1 import PyPowerFlexTestCase -class TestProtectionDomainClient(tests.PyPowerFlexTestCase): +class TestProtectionDomainClient(PyPowerFlexTestCase): """ Test class for the ProtectionDomainClient. """ diff --git a/tests/test_replication_consistency_group.py b/tests/gen1/test_replication_consistency_group.py similarity index 99% rename from tests/test_replication_consistency_group.py rename to tests/gen1/test_replication_consistency_group.py index fb3befe..d9a737a 100644 --- a/tests/test_replication_consistency_group.py +++ b/tests/gen1/test_replication_consistency_group.py @@ -18,10 +18,10 @@ # pylint: disable=invalid-name,too-many-public-methods from PyPowerFlex import exceptions -import tests +from tests.gen1 import PyPowerFlexTestCase -class TestReplicationConsistencyGroupClient(tests.PyPowerFlexTestCase): +class TestReplicationConsistencyGroupClient(PyPowerFlexTestCase): """ Tests for the ReplicationConsistencyGroupClient. """ diff --git a/tests/test_replication_pair.py b/tests/gen1/test_replication_pair.py similarity index 98% rename from tests/test_replication_pair.py rename to tests/gen1/test_replication_pair.py index d81a95e..e20af7a 100644 --- a/tests/test_replication_pair.py +++ b/tests/gen1/test_replication_pair.py @@ -18,10 +18,10 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -import tests +from tests.gen1 import PyPowerFlexTestCase -class TestReplicationPairClient(tests.PyPowerFlexTestCase): +class TestReplicationPairClient(PyPowerFlexTestCase): """ Test class for the ReplicationPairClient. """ diff --git a/tests/test_sdc.py b/tests/gen1/test_sdc.py similarity index 98% rename from tests/test_sdc.py rename to tests/gen1/test_sdc.py index 39f1f08..d5dc91d 100644 --- a/tests/test_sdc.py +++ b/tests/gen1/test_sdc.py @@ -18,10 +18,10 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -import tests +from tests.gen1 import PyPowerFlexTestCase -class TestSdcClient(tests.PyPowerFlexTestCase): +class TestSdcClient(PyPowerFlexTestCase): """ Tests for the SdcClient class. """ diff --git a/tests/test_sds.py b/tests/gen1/test_sds.py similarity index 98% rename from tests/test_sds.py rename to tests/gen1/test_sds.py index 8b9ac4b..032aa89 100644 --- a/tests/test_sds.py +++ b/tests/gen1/test_sds.py @@ -18,11 +18,11 @@ # pylint: disable=invalid-name,too-many-public-methods from PyPowerFlex import exceptions -from PyPowerFlex.objects import sds -import tests +from PyPowerFlex.gen1.objects import sds +from tests.gen1 import PyPowerFlexTestCase -class TestSdsClient(tests.PyPowerFlexTestCase): +class TestSdsClient(PyPowerFlexTestCase): """ Tests for the SdsClient class. """ diff --git a/tests/test_sdt.py b/tests/gen1/test_sdt.py similarity index 98% rename from tests/test_sdt.py rename to tests/gen1/test_sdt.py index 927d007..daf1fe3 100644 --- a/tests/test_sdt.py +++ b/tests/gen1/test_sdt.py @@ -18,11 +18,11 @@ # pylint: disable=invalid-name,too-many-public-methods from PyPowerFlex import exceptions -from PyPowerFlex.objects import sdt -import tests +from PyPowerFlex.gen1.objects import sdt +from tests.gen1 import PyPowerFlexTestCase -class TestSdtClient(tests.PyPowerFlexTestCase): +class TestSdtClient(PyPowerFlexTestCase): """ Tests for the SdtClient class. """ diff --git a/tests/test_service_template.py b/tests/gen1/test_service_template.py similarity index 96% rename from tests/test_service_template.py rename to tests/gen1/test_service_template.py index b0afa22..197cfa1 100644 --- a/tests/test_service_template.py +++ b/tests/gen1/test_service_template.py @@ -18,10 +18,10 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -import tests +from tests.gen1 import PyPowerFlexTestCase -class TestServiceTemplateClient(tests.PyPowerFlexTestCase): +class TestServiceTemplateClient(PyPowerFlexTestCase): """ Test class for the ServiceTemplateClient. """ diff --git a/tests/test_snapshot_policy.py b/tests/gen1/test_snapshot_policy.py similarity index 95% rename from tests/test_snapshot_policy.py rename to tests/gen1/test_snapshot_policy.py index 917650e..9927f7c 100644 --- a/tests/test_snapshot_policy.py +++ b/tests/gen1/test_snapshot_policy.py @@ -18,11 +18,11 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -from PyPowerFlex.objects import snapshot_policy as sp -import tests +from PyPowerFlex.gen1.objects import snapshot_policy as sp +from tests.gen1 import PyPowerFlexTestCase -class TestSnapshotPolicyClient(tests.PyPowerFlexTestCase): +class TestSnapshotPolicyClient(PyPowerFlexTestCase): """ Test class for snapshot policy client. """ @@ -59,7 +59,7 @@ def setUp(self): {}, '/types/SnapshotPolicy' '/instances/action/querySelectedStatistics': { - self.fake_policy_id: {'numOfSrcVols': 1} + self.fake_policy_id: {'numOfpypowerflexVols': 1} }, }, self.RESPONSE_MODE.Invalid: { @@ -231,9 +231,9 @@ def test_snapshot_policy_query_selected_statistics(self): Tests the behavior of the query_selected_statistics method. """ ret = self.client.snapshot_policy.query_selected_statistics( - properties=["numOfSrcVols"] + properties=["numOfpypowerflexVols"] ) - assert ret.get(self.fake_policy_id).get("numOfSrcVols") == 1 + assert ret.get(self.fake_policy_id).get("numOfpypowerflexVols") == 1 def test_snapshot_policy_query_selected_statistics_bad_status(self): """ @@ -244,5 +244,5 @@ def test_snapshot_policy_query_selected_statistics_bad_status(self): self.assertRaises( exceptions.PowerFlexFailQuerying, self.client.snapshot_policy.query_selected_statistics, - properties=["numOfSrcVols"], + properties=["numOfpypowerflexVols"], ) diff --git a/tests/test_storage_pool.py b/tests/gen1/test_storage_pool.py similarity index 98% rename from tests/test_storage_pool.py rename to tests/gen1/test_storage_pool.py index 0b0ba2c..640b72d 100644 --- a/tests/test_storage_pool.py +++ b/tests/gen1/test_storage_pool.py @@ -18,13 +18,13 @@ # pylint: disable=invalid-name,too-many-public-methods from PyPowerFlex import exceptions -from PyPowerFlex.objects.storage_pool import CompressionMethod -from PyPowerFlex.objects.storage_pool import ExternalAccelerationType -from PyPowerFlex.objects.storage_pool import MediaType -import tests +from PyPowerFlex.gen1.objects.storage_pool import CompressionMethod +from PyPowerFlex.gen1.objects.storage_pool import ExternalAccelerationType +from PyPowerFlex.gen1.objects.storage_pool import MediaType +from tests.gen1 import PyPowerFlexTestCase -class TestStoragePoolClient(tests.PyPowerFlexTestCase): +class TestStoragePoolClient(PyPowerFlexTestCase): """ Test class for the StoragePoolClient. """ diff --git a/tests/test_system.py b/tests/gen1/test_system.py similarity index 98% rename from tests/test_system.py rename to tests/gen1/test_system.py index b5f6e92..5ce9330 100644 --- a/tests/test_system.py +++ b/tests/gen1/test_system.py @@ -18,11 +18,11 @@ # pylint: disable=invalid-name,too-many-public-methods,duplicate-code from PyPowerFlex import exceptions -from PyPowerFlex.objects import system -import tests +from PyPowerFlex.gen1.objects import system +from tests.gen1 import PyPowerFlexTestCase -class TestSystemClient(tests.PyPowerFlexTestCase): +class TestSystemClient(PyPowerFlexTestCase): """ Test class for the SystemClient. """ diff --git a/tests/test_utility.py b/tests/gen1/test_utility.py similarity index 96% rename from tests/test_utility.py rename to tests/gen1/test_utility.py index a434a3f..eabedff 100644 --- a/tests/test_utility.py +++ b/tests/gen1/test_utility.py @@ -18,10 +18,10 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -import tests +from tests.gen1 import PyPowerFlexTestCase -class TestPowerFlexUtility(tests.PyPowerFlexTestCase): +class TestPowerFlexUtility(PyPowerFlexTestCase): """ Test class for the PowerFlex utility. """ diff --git a/tests/test_volume.py b/tests/gen1/test_volume.py similarity index 98% rename from tests/test_volume.py rename to tests/gen1/test_volume.py index 0944a3d..2dcd9a2 100644 --- a/tests/test_volume.py +++ b/tests/gen1/test_volume.py @@ -18,11 +18,11 @@ # pylint: disable=invalid-name,too-many-public-methods from PyPowerFlex import exceptions -from PyPowerFlex.objects import volume -import tests +from PyPowerFlex.gen1.objects import volume +# import tests +from tests.gen1 import PyPowerFlexTestCase - -class TestVolumeClient(tests.PyPowerFlexTestCase): +class TestVolumeClient(PyPowerFlexTestCase): """ Test class for the volume client. """ diff --git a/tests/gen2/__init__.py b/tests/gen2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/gen2/test_storage_node.py b/tests/gen2/test_storage_node.py new file mode 100644 index 0000000..a5dc59d --- /dev/null +++ b/tests/gen2/test_storage_node.py @@ -0,0 +1,313 @@ +# 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 Storage Node client.""" + +# pylint: disable=invalid-name,too-many-public-methods + +from PyPowerFlex import exceptions +from PyPowerFlex.gen2.objects import storage_node +from tests.gen1 import PyPowerFlexTestCase + + +class TestStorageNodeClient(PyPowerFlexTestCase): + """ + Tests for the StorageNodeClient class. + """ + def setUp(self): + """ + Set up the test environment. + """ + super().setUp() + self.client.initialize() + self.fake_node_id = '1' + self.fake_sp_id = '1' + self.fake_pd_id = '1' + self.fake_node_ips = [storage_node.StorageNodeIp( + '1.2.3.4', storage_node.StorageNodeIpRoles.storage_and_app)] + + self.MOCK_RESPONSES = { + self.RESPONSE_MODE.Valid: { + f'/types/{storage_node.StorageNode.entity}/instances': + {'id': self.fake_sds_id}, + f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}': + {'id': self.fake_sds_id}, + f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/addIp': + {}, + f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/removeSds': + {}, + f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/relationships/Device': + [], + f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/setSdsName': + {}, + f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/removeSdsIp': + {}, + f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/setSdsIpRole': + {}, + f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/setSdsPort': + {}, + f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/enableRfcache': + {}, + f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/disableRfcache': + {}, + f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/setSdsRmcacheEnabled': + {}, + f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/setSdsRmcacheSize': + {}, + f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/setSdsPerformanceParameters': + {}, + f'/types/{storage_node.StorageNode.entity}' + '/instances/action/querySelectedStatistics': { + self.fake_sds_id: {'rfcacheFdReadTimeGreater5Sec': 0} + }, + }, + self.RESPONSE_MODE.Invalid: { + '/types/Sds/instances': + {}, + } + } + + def test_sds_add_ip(self): + """ + Test the add_ip method of the SdsClient. + """ + self.client.sds.add_ip(self.fake_sds_id, self.fake_sds_ips[0]) + + def test_sds_add_ip_bad_status(self): + """ + Test the add_ip method of the SdsClient with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.sds.add_ip, + self.fake_sds_id, + self.fake_sds_ips[0]) + + def test_sds_create(self): + """ + Test the create method of the SdsClient. + """ + self.client.sds.create(protection_domain_id=self.fake_pd_id, + sds_ips=self.fake_sds_ips) + + def test_sds_create_bad_status(self): + """ + Test the create method of the SdsClient with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexFailCreating, + self.client.sds.create, + protection_domain_id=self.fake_pd_id, + sds_ips=self.fake_sds_ips) + + def test_sds_create_no_id_in_response(self): + """ + Test the create method of the SdsClient with no ID in the response. + """ + with self.http_response_mode(self.RESPONSE_MODE.Invalid): + self.assertRaises(KeyError, + self.client.sds.create, + protection_domain_id=self.fake_pd_id, + sds_ips=self.fake_sds_ips) + + def test_sds_delete(self): + """ + Test the delete method of the SdsClient. + """ + self.client.sds.delete(self.fake_sds_id) + + def test_sds_delete_bad_status(self): + """ + Test the delete method of the SdsClient with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexFailDeleting, + self.client.sds.delete, + self.fake_sds_id) + + def test_sds_get_devices(self): + """ + Test the get_devices method of the SdsClient. + """ + self.client.sds.get_devices(self.fake_sds_id) + + def test_sds_get_devices_bad_status(self): + """ + Test the get_devices method of the SdsClient with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.sds.get_devices, + self.fake_sds_id) + + def test_sds_rename(self): + """ + Test the rename method of the SdsClient. + """ + self.client.sds.rename(self.fake_sds_id, name='new_name') + + def test_sds_rename_bad_status(self): + """ + Test the rename method of the SdsClient with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexFailRenaming, + self.client.sds.rename, + self.fake_sds_id, + name='new_name') + + def test_sds_remove_ip(self): + """ + Test the remove_ip method of the SdsClient. + """ + self.client.sds.remove_ip(self.fake_sds_id, ip='1.2.3.4') + + def test_sds_remove_ip_bad_status(self): + """ + Test the remove_ip method of the SdsClient with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.sds.remove_ip, + self.fake_sds_id, + ip='1.2.3.4') + + def test_sds_set_ip_role(self): + """ + Test the set_ip_role method. + """ + self.client.sds.set_ip_role(self.fake_sds_id, + ip='1.2.3.4', + role=sds.SdsIpRoles.sdc_only, + force=True) + + def test_sds_set_ip_role_bad_status(self): + """ + Test the set_ip_role method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.sds.set_ip_role, + self.fake_sds_id, + ip='1.2.3.4', + role=sds.SdsIpRoles.sdc_only, + force=True) + + def test_sds_set_port(self): + """ + Test the set_port method. + """ + self.client.sds.set_port(self.fake_sds_id, sds_port=4443) + + def test_sds_set_port_bad_status(self): + """ + Test the set_port method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.sds.set_port, + self.fake_sds_id, + sds_port=4443) + + def test_sds_set_rfcache_enabled(self): + """ + Test the set_rfcache_enabled method. + """ + self.client.sds.set_rfcache_enabled(self.fake_sds_id, + rfcache_enabled=True) + + def test_sds_set_rfcache_enabled_bad_status(self): + """ + Test the set_rfcache_enabled method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.sds.set_rfcache_enabled, + self.fake_sds_id, + rfcache_enabled=True) + + def test_sds_set_rmcache_enabled(self): + """ + Test the set_rmcache_enabled method. + """ + self.client.sds.set_rmcache_enabled(self.fake_sds_id, + rmcache_enabled=True) + + def test_sds_set_rmcache_enabled_bad_status(self): + """ + Test the set_rmcache_enabled method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.sds.set_rmcache_enabled, + self.fake_sds_id, + rmcache_enabled=True) + + def test_sds_set_rmcache_size(self): + """ + Test the set_rmcache_size method. + """ + self.client.sds.set_rmcache_size(self.fake_sds_id, + rmcache_size=128) + + def test_sds_set_rmcache_size_bad_status(self): + """ + Test the set_rmcache_size method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.sds.set_rmcache_size, + self.fake_sds_id, + rmcache_size=128) + + def test_sds_set_performance_parameters(self): + """ + Test the set_performance_parameters method. + """ + self.client.sds.set_performance_parameters( + self.fake_sds_id, + performance_profile=sds.PerformanceProfile.highperformance) + + def test_sds_set_performance_parameters_bad_status(self): + """ + Test the set_performance_parameters method with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises( + exceptions.PowerFlexClientException, + self.client.sds.set_performance_parameters, + self.fake_sds_id, + performance_profile=sds.PerformanceProfile.highperformance) + + def test_sds_query_selected_statistics(self): + """ + Test the query_selected_statistics method. + """ + ret = self.client.sds.query_selected_statistics( + properties=["rfcacheFdReadTimeGreater5Sec"] + ) + assert ret.get(self.fake_sds_id).get( + "rfcacheFdReadTimeGreater5Sec") == 0 + + def test_sds_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.sds.query_selected_statistics, + properties=["rfcacheFdReadTimeGreater5Sec"], + ) From c0285deeb35d13acf0827d373b3859a5206b7190 Mon Sep 17 00:00:00 2001 From: Yiming Bao Date: Wed, 23 Jul 2025 15:40:05 +0800 Subject: [PATCH 02/15] refactor --- PyPowerFlex/__init__.py | 22 +- PyPowerFlex/base_client.py | 1 + PyPowerFlex/constants.py | 4 + PyPowerFlex/gen1/objects/__init__.py | 59 -- PyPowerFlex/gen2/objects/sdc.py | 94 --- PyPowerFlex/gen2/objects/sdt.py | 345 ----------- PyPowerFlex/gen2/objects/system.py | 459 -------------- PyPowerFlex/gen2/objects/utility.py | 140 ----- PyPowerFlex/gen2/objects/volume.py | 574 ------------------ .../objects => objects/common}/__init__.py | 14 +- .../{gen1/objects => objects/common}/host.py | 0 .../{gen1/objects => objects/common}/sdc.py | 0 .../{gen1/objects => objects/common}/sdt.py | 0 .../objects => objects/common}/system.py | 0 PyPowerFlex/objects/gen1/__init__.py | 51 ++ .../gen1}/acceleration_pool.py | 0 .../objects => objects/gen1}/deployment.py | 0 .../{gen1/objects => objects/gen1}/device.py | 0 .../objects => objects/gen1}/fault_set.py | 0 .../gen1}/firmware_repository.py | 0 .../gen1}/managed_device.py | 0 .../gen1}/protection_domain.py | 0 .../gen1}/replication_consistency_group.py | 0 .../gen1}/replication_pair.py | 0 .../{gen1/objects => objects/gen1}/sds.py | 0 .../gen1}/service_template.py | 0 .../gen1}/snapshot_policy.py | 0 .../objects => objects/gen1}/storage_pool.py | 2 +- .../{gen1/objects => objects/gen1}/utility.py | 0 .../{gen1/objects => objects/gen1}/volume.py | 0 PyPowerFlex/objects/gen2/__init__.py | 24 + .../objects => objects/gen2}/storage_node.py | 0 PyPowerFlex/objects/gen2/utility.py | 69 +++ setup.py | 6 +- tests/__init__.py | 0 tests/common/__init__.py | 244 ++++++++ tests/{gen1 => common}/test_host.py | 0 tests/{gen1 => common}/test_sdc.py | 0 tests/{gen1 => common}/test_sdt.py | 2 +- tests/{gen1 => common}/test_system.py | 2 +- tests/gen1/test_acceleration_pool.py | 4 +- tests/gen1/test_deployment.py | 2 +- tests/gen1/test_device.py | 4 +- tests/gen1/test_fault_set.py | 2 +- tests/gen1/test_firmware_repository.py | 2 +- tests/gen1/test_managed_device.py | 2 +- tests/gen1/test_protection_domain.py | 4 +- .../test_replication_consistency_group.py | 2 +- tests/gen1/test_replication_pair.py | 2 +- tests/gen1/test_sds.py | 4 +- tests/gen1/test_service_template.py | 2 +- tests/gen1/test_snapshot_policy.py | 4 +- tests/gen1/test_storage_pool.py | 8 +- tests/gen1/test_utility.py | 2 +- tests/gen1/test_volume.py | 5 +- tests/gen2/__init__.py | 235 +++++++ tests/gen2/test_storage_node.py | 217 ++++--- 57 files changed, 781 insertions(+), 1832 deletions(-) delete mode 100644 PyPowerFlex/gen1/objects/__init__.py delete mode 100644 PyPowerFlex/gen2/objects/sdc.py delete mode 100644 PyPowerFlex/gen2/objects/sdt.py delete mode 100644 PyPowerFlex/gen2/objects/system.py delete mode 100644 PyPowerFlex/gen2/objects/utility.py delete mode 100644 PyPowerFlex/gen2/objects/volume.py rename PyPowerFlex/{gen2/objects => objects/common}/__init__.py (66%) rename PyPowerFlex/{gen1/objects => objects/common}/host.py (100%) rename PyPowerFlex/{gen1/objects => objects/common}/sdc.py (100%) rename PyPowerFlex/{gen1/objects => objects/common}/sdt.py (100%) rename PyPowerFlex/{gen1/objects => objects/common}/system.py (100%) create mode 100644 PyPowerFlex/objects/gen1/__init__.py rename PyPowerFlex/{gen1/objects => objects/gen1}/acceleration_pool.py (100%) rename PyPowerFlex/{gen1/objects => objects/gen1}/deployment.py (100%) rename PyPowerFlex/{gen1/objects => objects/gen1}/device.py (100%) rename PyPowerFlex/{gen1/objects => objects/gen1}/fault_set.py (100%) rename PyPowerFlex/{gen1/objects => objects/gen1}/firmware_repository.py (100%) rename PyPowerFlex/{gen1/objects => objects/gen1}/managed_device.py (100%) rename PyPowerFlex/{gen1/objects => objects/gen1}/protection_domain.py (100%) rename PyPowerFlex/{gen1/objects => objects/gen1}/replication_consistency_group.py (100%) rename PyPowerFlex/{gen1/objects => objects/gen1}/replication_pair.py (100%) rename PyPowerFlex/{gen1/objects => objects/gen1}/sds.py (100%) rename PyPowerFlex/{gen1/objects => objects/gen1}/service_template.py (100%) rename PyPowerFlex/{gen1/objects => objects/gen1}/snapshot_policy.py (100%) rename PyPowerFlex/{gen1/objects => objects/gen1}/storage_pool.py (99%) rename PyPowerFlex/{gen1/objects => objects/gen1}/utility.py (100%) rename PyPowerFlex/{gen1/objects => objects/gen1}/volume.py (100%) create mode 100644 PyPowerFlex/objects/gen2/__init__.py rename PyPowerFlex/{gen2/objects => objects/gen2}/storage_node.py (100%) create mode 100644 PyPowerFlex/objects/gen2/utility.py delete mode 100644 tests/__init__.py create mode 100644 tests/common/__init__.py rename tests/{gen1 => common}/test_host.py (100%) rename tests/{gen1 => common}/test_sdc.py (100%) rename tests/{gen1 => common}/test_sdt.py (99%) rename tests/{gen1 => common}/test_system.py (99%) diff --git a/PyPowerFlex/__init__.py b/PyPowerFlex/__init__.py index e878829..d67d109 100644 --- a/PyPowerFlex/__init__.py +++ b/PyPowerFlex/__init__.py @@ -23,8 +23,9 @@ from PyPowerFlex import exceptions from PyPowerFlex import token from PyPowerFlex import utils -import PyPowerFlex.gen1.objects as gen1 -import PyPowerFlex.gen2.objects as gen2 +import PyPowerFlex.objects.common as common +import PyPowerFlex.objects.gen1 as gen1 +import PyPowerFlex.objects.gen2 as gen2 __all__ = [ 'PowerFlexClient' @@ -101,10 +102,8 @@ def initialize(self): Raises: PowerFlexClientException: If the PowerFlex API version is lower than 3.0. """ - # unchanged resources here - self.__add_storage_entity('system', gen1.System) - self.__add_storage_entity('sdc', gen1.Sdc) - self.__add_storage_entity('sdt', gen1.Sdt) + # common objects here + self.add_objects_common() self.configuration.validate() utils.init_logger(self.configuration.log_level) @@ -117,10 +116,16 @@ def initialize(self): if version.parse(self.system.api_version()) > version.Version('3.0') and \ version.parse(self.system.api_version()) < version.Version('5.0'): self.add_objects_gen1() - elif version.parse(self.system.api_version()) > version.Version('5.0'): + elif version.parse(self.system.api_version()) >= version.Version('5.0'): self.add_objects_gen2() self.__is_initialized = True + def add_objects_common(self): + self.__add_storage_entity('system', common.System) + self.__add_storage_entity('sdc', common.Sdc) + self.__add_storage_entity('sdt', common.Sdt) + self.__add_storage_entity('host', common.Host) + def add_objects_gen1(self): self.__add_storage_entity('device', gen1.Device) self.__add_storage_entity( @@ -145,8 +150,7 @@ def add_objects_gen1(self): self.__add_storage_entity( 'firmware_repository', gen1.FirmwareRepository) - self.__add_storage_entity('host', gen1.Host) def add_objects_gen2(self): self.__add_storage_entity('storage_node', gen2.StorageNode) - # self.__add_storage_entity('volume', gen2.Volume) + self.__add_storage_entity('utility', gen2.PowerFlexUtility) diff --git a/PyPowerFlex/base_client.py b/PyPowerFlex/base_client.py index 79ccb9d..8bce0e2 100644 --- a/PyPowerFlex/base_client.py +++ b/PyPowerFlex/base_client.py @@ -373,6 +373,7 @@ class EntityRequest(Request): base_type_special_action_url = '/types/{entity}/instances/action/{action}' query_mdm_cluster_url = '/instances/{entity}/queryMdmCluster' list_statistics_url = '/types/{entity}/instances/action/{action}' + metrics_query_url = '/dtapi/rest/v1/metrics/query' service_template_url = '/V1/ServiceTemplate' managed_device_url = '/V1/ManagedDevice' deployment_url = '/V1/Deployment' diff --git a/PyPowerFlex/constants.py b/PyPowerFlex/constants.py index ee56ff7..a71f05b 100644 --- a/PyPowerFlex/constants.py +++ b/PyPowerFlex/constants.py @@ -240,6 +240,10 @@ class StoragePoolConstants: DEFAULT_STATISTICS_PROPERTIES_ABOVE_3_5 = [ "thinCapacityAllocatedInKm", "thinUserDataCapacityInKb"] + DEFAULT_QUERY_METRICS = [ + "" + ] + class VolumeConstants: """ diff --git a/PyPowerFlex/gen1/objects/__init__.py b/PyPowerFlex/gen1/objects/__init__.py deleted file mode 100644 index 31d7e61..0000000 --- a/PyPowerFlex/gen1/objects/__init__.py +++ /dev/null @@ -1,59 +0,0 @@ -# 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. - -"""This module contains the objects for interacting with the PowerFlex APIs.""" - -from PyPowerFlex.gen1.objects.device import Device -from PyPowerFlex.gen1.objects.fault_set import FaultSet -from PyPowerFlex.gen1.objects.protection_domain import ProtectionDomain -from PyPowerFlex.gen1.objects.sdc import Sdc -from PyPowerFlex.gen1.objects.sds import Sds -from PyPowerFlex.gen1.objects.sdt import Sdt -from PyPowerFlex.gen1.objects.snapshot_policy import SnapshotPolicy -from PyPowerFlex.gen1.objects.storage_pool import StoragePool -from PyPowerFlex.gen1.objects.acceleration_pool import AccelerationPool -from PyPowerFlex.gen1.objects.system import System -from PyPowerFlex.gen1.objects.volume import Volume -from PyPowerFlex.gen1.objects.utility import PowerFlexUtility -from PyPowerFlex.gen1.objects.replication_consistency_group import ReplicationConsistencyGroup -from PyPowerFlex.gen1.objects.replication_pair import ReplicationPair -from PyPowerFlex.gen1.objects.service_template import ServiceTemplate -from PyPowerFlex.gen1.objects.managed_device import ManagedDevice -from PyPowerFlex.gen1.objects.deployment import Deployment -from PyPowerFlex.gen1.objects.firmware_repository import FirmwareRepository -from PyPowerFlex.gen1.objects.host import Host - - -__all__ = [ - 'Device', - 'FaultSet', - 'ProtectionDomain', - 'Sdc', - 'Sds', - 'Sdt', - 'SnapshotPolicy', - 'StoragePool', - 'AccelerationPool', - 'System', - 'Volume', - 'PowerFlexUtility', - 'ReplicationConsistencyGroup', - 'ReplicationPair', - 'ServiceTemplate', - 'ManagedDevice', - 'Deployment', - 'FirmwareRepository', - 'Host', -] diff --git a/PyPowerFlex/gen2/objects/sdc.py b/PyPowerFlex/gen2/objects/sdc.py deleted file mode 100644 index 614c123..0000000 --- a/PyPowerFlex/gen2/objects/sdc.py +++ /dev/null @@ -1,94 +0,0 @@ -# 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 SDC APIs.""" - -import logging -from PyPowerFlex import base_client - - -LOG = logging.getLogger(__name__) - - -class Sdc(base_client.EntityRequest): - """ - A class representing SDC client. - """ - def delete(self, sdc_id): - """Remove PowerFlex SDC. - - :type sdc_id: str - :rtype: None - """ - - return self._delete_entity(sdc_id) - - def get_mapped_volumes(self, sdc_id, filter_fields=None, fields=None): - """Get PowerFlex volumes mapped to SDC. - - :type sdc_id: str - :type filter_fields: dict - :type fields: list|tuple - :rtype: list[dict] - """ - - return self.get_related(sdc_id, 'Volume', filter_fields, fields) - - def rename(self, sdc_id, name): - """Rename PowerFlex SDC. - - :type sdc_id: str - :type name: str - :rtype: dict - """ - - action = 'setSdcName' - - params = {"sdcName": name} - - return self._rename_entity(action, sdc_id, params) - - def set_performance_profile(self, sdc_id, perf_profile): - """Apply a performance profile to the specified SDC. - - :type sdc_id: str - :type perf_profile: str - :rtype: dict - """ - - action = 'setSdcPerformanceParameters' - - params = {"perfProfile": perf_profile} - return self._perform_entity_operation_based_on_action( - sdc_id, action, params=params, add_entity=False) - - def query_selected_statistics(self, properties, ids=None): - """Query PowerFlex SDC statistics. - - :type properties: list - :type ids: list of SDC IDs or None for all SDC - :rtype: dict - """ - - action = "querySelectedStatistics" - - params = {'properties': properties} - - if ids: - params["ids"] = ids - else: - params["allIds"] = "" - - return self._query_selected_statistics(action, params) diff --git a/PyPowerFlex/gen2/objects/sdt.py b/PyPowerFlex/gen2/objects/sdt.py deleted file mode 100644 index 40d7878..0000000 --- a/PyPowerFlex/gen2/objects/sdt.py +++ /dev/null @@ -1,345 +0,0 @@ -# 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 SDT APIs.""" - -# pylint: disable=too-few-public-methods,no-member,too-many-arguments,too-many-positional-arguments - -import logging -import requests -from PyPowerFlex import base_client -from PyPowerFlex import exceptions -from PyPowerFlex import utils - -LOG = logging.getLogger(__name__) - - -class SdtIp(dict): - """PowerFlex sdt ip object. - - JSON-serializable, should be used as `sdt_ips` list item - in `Sdt.create` method or sdt_ip item in `Sdt.add_sdt_ip` method. - """ - - def __init__(self, ip, role): - params = utils.prepare_params( - { - "ip": ip, - "role": role, - }, - dump=False, - ) - super().__init__(**params) - - -class SdtIpRoles: - """SDT ip roles.""" - - storage_only = "StorageOnly" - host_only = "HostOnly" - storage_and_host = "StorageAndHost" - - -class Sdt(base_client.EntityRequest): - """ - A class representing SDT client. - """ - def create( - self, - sdt_ips, - sdt_name, - protection_domain_id, - storage_port=None, - nvme_port=None, - discovery_port=None, - ): - """Create PowerFlex SDT. - - :type sdt_ips: list[dict] - :type storage_port: int - :type nvme_port: int - :type discovery_port: int - :type sdt_name: str - :type protection_domain_id: str - :rtype: dict - """ - - params = { - "ips": sdt_ips, - "storagePort": storage_port, - "nvmePort": nvme_port, - "discoveryPort": discovery_port, - "name": sdt_name, - "protectionDomainId": protection_domain_id, - } - - return self._create_entity(params) - - def rename(self, sdt_id, name): - """Rename PowerFlex SDT. - - :type sdt_id: str - :type name: str - :rtype: dict - """ - - action = "renameSdt" - - params = {'newName': name} - - return self._rename_entity(action, sdt_id, params) - - def add_ip(self, sdt_id, ip, role): - """Add PowerFlex SDT target IP address. - - :type sdt_id: str - :type ip: str - :type role: str - :rtype: dict - """ - - action = "addIp" - - params = { - "ip": ip, - "role": role, - } - - r, response = self.send_post_request( - self.base_action_url, - action=action, - entity=self.entity, - entity_id=sdt_id, - params=params, - ) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to add IP for PowerFlex {self.entity} " - f"with id {sdt_id}. Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return self.get(entity_id=sdt_id) - - def remove_ip(self, sdt_id, ip): - """Remove PowerFlex SDT target IP address. - - :type sdt_id: str - :type ip: str - :rtype: dict - """ - - action = "removeIp" - - params = {"ip": ip} - - r, response = self.send_post_request( - self.base_action_url, - action=action, - entity=self.entity, - entity_id=sdt_id, - params=params, - ) - if r.status_code != requests.codes.ok: - msg = f"Failed to remove IP from PowerFlex {self.entity} " \ - f"with id {sdt_id}. Error: {response}" - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return self.get(entity_id=sdt_id) - - def set_ip_role(self, sdt_id, ip, role): - """Set PowerFlex SDT target IP address role. - - :type sdt_id: str - :type ip: str - :param role: one of predefined attributes of SdtIpRoles - :type role: str - :rtype: dict - """ - - action = "modifyIpRole" - - params = { - "ip": ip, - "newRole": role - } - - r, response = self.send_post_request( - self.base_action_url, - action=action, - entity=self.entity, - entity_id=sdt_id, - params=params, - ) - if r.status_code != requests.codes.ok: - msg = f"Failed to set ip role for PowerFlex {self.entity} " \ - f"with id {sdt_id}. Error: {response}" - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return self.get(entity_id=sdt_id) - - def set_storage_port(self, sdt_id, storage_port): - """Set PowerFlex SDT storage port. - - :type sdt_id: str - :type storage_port: int - :rtype: dict - """ - - action = "modifyStoragePort" - - params = {"newStoragePort": storage_port} - - r, response = self.send_post_request( - self.base_action_url, - action=action, - entity=self.entity, - entity_id=sdt_id, - params=params, - ) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to set storage port for PowerFlex {self.entity} " - f"with id {sdt_id}. Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return self.get(entity_id=sdt_id) - - def set_nvme_port(self, sdt_id, nvme_port): - """Set PowerFlex SDT NVMe port. - - :type sdt_id: str - :type nvme_port: int - :rtype: dict - """ - - action = "modifyNvmePort" - - params = {"newNvmePort": nvme_port} - - r, response = self.send_post_request( - self.base_action_url, - action=action, - entity=self.entity, - entity_id=sdt_id, - params=params, - ) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to set nvme port for PowerFlex {self.entity} " - f"with id {sdt_id}. Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return self.get(entity_id=sdt_id) - - def set_discovery_port(self, sdt_id, discovery_port): - """Set PowerFlex SDT discovery port. - - :type sdt_id: str - :type discovery_port: int - :rtype: dict - """ - - action = "modifyDiscoveryPort" - - params = {"newDiscoveryPort": discovery_port} - - r, response = self.send_post_request( - self.base_action_url, - action=action, - entity=self.entity, - entity_id=sdt_id, - params=params, - ) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to set discovery port for PowerFlex {self.entity} " - f"with id {sdt_id}. Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return self.get(entity_id=sdt_id) - - def enter_maintenance_mode(self, sdt_id): - """Enter Maintenance Mode. - - :type sdt_id: str - :rtype: dict - """ - - action = "enterMaintenanceMode" - - r, response = self.send_post_request( - self.base_action_url, - action=action, - entity=self.entity, - entity_id=sdt_id, - params=None, - ) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to enter maintenance mode for PowerFlex {self.entity} " - f"with id {sdt_id}. Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return self.get(entity_id=sdt_id) - - def exit_maintenance_mode(self, sdt_id): - """Exit Maintenance Mode. - - :type sdt_id: str - :rtype: dict - """ - - action = "exitMaintenanceMode" - - r, response = self.send_post_request( - self.base_action_url, - action=action, - entity=self.entity, - entity_id=sdt_id, - params=None, - ) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to exit maintenance mode for PowerFlex {self.entity} " - f"with id {sdt_id}. Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return self.get(entity_id=sdt_id) - - def delete(self, sdt_id, force=None): - """Remove PowerFlex SDT. - - :type sdt_id: str - :type force: bool - :rtype: None - """ - - params = {"force": force} - - return self._delete_entity(sdt_id, params) diff --git a/PyPowerFlex/gen2/objects/system.py b/PyPowerFlex/gen2/objects/system.py deleted file mode 100644 index 40b7957..0000000 --- a/PyPowerFlex/gen2/objects/system.py +++ /dev/null @@ -1,459 +0,0 @@ -# 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 - -import logging -import re - -import requests - -from PyPowerFlex import base_client -from PyPowerFlex import exceptions -from PyPowerFlex import utils - - -LOG = logging.getLogger(__name__) - - -class SnapshotDef(dict): - """PowerFlex definition of snapshot to create. - - JSON-serializable, should be used as `snapshot_defs` list item - in `System.snapshot_volumes` method. - """ - - def __init__(self, volume_id, name=None): - """Initialize SnapshotDef object. - - :type volume_id: str - :type name: str - """ - - params = utils.prepare_params( - { - 'volumeId': volume_id, - 'snapshotName': name, - }, - dump=False - ) - super().__init__(**params) - - -class System(base_client.EntityRequest): - """Client for system operations""" - def __init__(self, token, configuration): - self.__api_version = None - super().__init__(token, configuration) - - def api_version(self, cached=True): - """Get PowerFlex API version. - - :param cached: get version from cache or send API response - :type cached: bool - :rtype: str - """ - - url = '/version' - - if not self.__api_version or not cached: - r, response = self.send_get_request(url) - if r.status_code != requests.codes.ok: - exc = exceptions.PowerFlexFailQuerying('API version') - LOG.error(exc.message) - raise exc - pattern = re.compile(r'^\d+(\.\d+)*$') - if not pattern.match(response): - msg = ( - f"Failed to query PowerFlex API version. Invalid version " - f"format: {response}." - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - self.__api_version = response - return self.__api_version - - def remove_cg_snapshots(self, system_id, cg_id, allow_ext_managed=None): - """Remove PowerFlex ConsistencyGroup snapshots. - - :type system_id: str - :type cg_id: str - :type allow_ext_managed: bool - :rtype: dict - """ - - action = 'removeConsistencyGroupSnapshots' - - params = { - "snapGroupId": cg_id, - "allowOnExtManagedVol": allow_ext_managed - } - - 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 remove consistency group snapshots from " - f"PowerFlex {self.entity} with id {system_id}. " - f"Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - 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): - """ - Add a standby MDM to the system. - :param mdm_ips: List of ip addresses assigned to new MDM. It can - contain IPv4 addresses. - :type mdm_ips: list[str] - :param role: Role of the new MDM. - :type role: str - :param management_ips: List of IP addresses used to manage the MDM. - It can contain IPv4 addresses. - :type management_ips: list[str] - :param port: Port of new MDM. Default: 9011 - :type port: str - :param mdm_name: Name of the new MDM. - :type mdm_name: str - :param allow_multiple_ips: Allow the added node to have a different - number of IPs from the primary node. - :type allow_multiple_ips: str - :param clean: Clean a previous MDM configuration. - :type clean: str - :param virtual_interface: List of NIC interfaces that will be used - for virtual IP address. - :type virtual_interface: list[str] - :return: ID of new standby MDM. - :rtype: dict - """ - action = 'addStandbyMdm' - params = { - "ips": mdm_ips, - "role": role, - "managementIps": management_ips, - "name": mdm_name, - "port": port, - "allowAsymmetricIps": allow_multiple_ips, - "forceClean": clean, - "virtIpIntfs": virtual_interface - } - - r, response = self.send_post_request(self.base_object_url, - action=action, - entity=self.entity, - params=params) - if r.status_code != requests.codes.ok: - msg = f"Failed to add standBy MDM on PowerFlex {self.entity}. " \ - f"Error: {response}" - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return response - - def remove_standby_mdm(self, mdm_id): - """ - Remove a standby MDM from the system. - :param mdm_id: ID of MDM to be removed. - :type mdm_id: str - :return: None - """ - action = 'removeStandbyMdm' - params = { - "id": mdm_id - } - - r, response = self.send_mdm_cluster_post_request(self.base_object_url, - action=action, - entity=self.entity, - params=params) - if r.status_code != requests.codes.ok and response is not None: - msg = ( - f"Failed to remove standBy MDM from PowerFlex {self.entity}. " - f"Error: {response}." - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return True - - def get_mdm_cluster_details(self): - """ - Get the MDM cluster details - :return: MDM cluster details - :rtype: dict - """ - - r, response = self.send_post_request(self.query_mdm_cluster_url, - entity=self.entity) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to get MDM cluster details on PowerFlex {self.entity}. " - f"Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return response - - def get_gateway_configuration_details(self): - """ - Get the gateway configuration details - :return: Gateway configuration details - :rtype: dict - """ - - r, response = self.send_get_request('/Configuration') - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to get gateway configuration details on PowerFlex {self.entity}. " - f"Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return response - - def change_mdm_ownership(self, mdm_id): - """ - Change MDM cluster ownership from current master MDM to different MDM. - - :param mdm_id: ID of New Manager MDM - :type mdm_id: str - :return: None - :rtype: dict - """ - action = 'changeMdmOwnership' - params = { - "id": mdm_id - } - - r, response = self.send_post_request(self.base_object_url, - action=action, - entity=self.entity, - params=params) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to change ownership on PowerFlex {self.entity}. " - f"Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - def set_cluster_mdm_performance_profile(self, performance_profile): - """ - Set the Cluster MDMs performance profile. - - :param performance_profile: Define the performance profile of MDMs. - :type performance_profile: str - :return: None - :rtype: dict - """ - action = 'setMdmPerformanceParameters' - params = {"perfProfile": performance_profile} - - r, response = self.send_post_request(self.base_object_url, - action=action, - entity=self.entity, - params=params) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to set performance profile of MDMs on PowerFlex " - f"{self.entity}. Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return response - - def rename_mdm(self, mdm_id, mdm_new_name): - """ - Set the Cluster MDMs performance profile. - - :param mdm_id: ID of MDM. - :type mdm_id: str - :param mdm_new_name: new name of MDM. - :type mdm_new_name: str - :return: None - :rtype: dict - """ - action = 'renameMdm' - params = { - "id": mdm_id, - "newName": mdm_new_name - } - - r, response = self.send_post_request(self.base_object_url, - action=action, - entity=self.entity, - params=params) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to rename the MDM on PowerFlex {self.entity}. " - f"Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return response - - def modify_virtual_ip_interface(self, mdm_id, virtual_ip_interfaces=None, - clear_interfaces=None): - """ - Set the Cluster MDMs performance profile. - - :param mdm_id: ID of MDM. - :type mdm_id: str - :param virtual_ip_interfaces: List of interface names to be used for - the MDM virtual IPs. - :type virtual_ip_interfaces: list[str] - :param clear_interfaces: Clear all virtual IP interfaces. - :type mdm_id: str - :return: None - :rtype: dict - """ - action = 'modifyVirtualIpInterfaces' - if virtual_ip_interfaces is not None: - params = { - "id": mdm_id, - "virtIpIntfs": virtual_ip_interfaces - } - else: - params = { - "id": mdm_id, - "clear": clear_interfaces - } - - r, response = self.send_post_request(self.base_object_url, - action=action, - entity=self.entity, - params=params) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to modify virtual IP interface on PowerFlex " - f"{self.entity}. Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return response - - def switch_cluster_mode( - self, - cluster_mode, - add_secondary=None, - remove_secondary=None, - add_tb=None, - remove_tb=None): - """ - Switch cluster mode. - - :param cluster_mode: New mode of MDM cluster - :type cluster_mode: str - :param add_secondary: List of secondary MDM IDs that will be part of - the cluster. A maximum of two IDs are allowed. - :type add_secondary: list[str] - :param remove_secondary: List of secondary MDM IDs that will be removed - from the cluster. - :type remove_secondary: list[str] - :param add_tb: List of TieBreaker MDM IDs that will be part of the - cluster. - :type add_tb: list[str] - :param remove_tb: List of TieBreaker MDM IDs that will be removed - from the cluster. - :type remove_tb: list[str] - :return: None - """ - action = 'switchClusterMode' - params = { - "mode": cluster_mode, - "addSlaveMdmIdList": add_secondary, - "addTBIdList": add_tb, - "removeSlaveMdmIdList": remove_secondary, - "removeTBIdList": remove_tb - } - - r, response = self.send_mdm_cluster_post_request(self.base_object_url, - action=action, - entity=self.entity, - params=params) - if r.status_code != requests.codes.ok and response is not None: - msg = f"Failed to switch MDM cluster mode PowerFlex {self.entity}. " \ - f"Error: {response}." - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return True - - def query_selected_statistics(self, properties): - """Query PowerFlex system statistics. - - :type properties: list - :rtype: dict - """ - - action = "querySelectedStatistics" - - params = {'properties': properties} - - return self._query_selected_statistics(action, params) diff --git a/PyPowerFlex/gen2/objects/utility.py b/PyPowerFlex/gen2/objects/utility.py deleted file mode 100644 index db60eea..0000000 --- a/PyPowerFlex/gen2/objects/utility.py +++ /dev/null @@ -1,140 +0,0 @@ -# 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. - -"""Utility module for PowerFlex.""" - -# pylint: disable=no-member,useless-parent-delegation -import logging - -import requests - -from PyPowerFlex import base_client -from PyPowerFlex import exceptions -from PyPowerFlex.constants import StoragePoolConstants, VolumeConstants, SnapshotPolicyConstants - - -LOG = logging.getLogger(__name__) - - -class PowerFlexUtility(base_client.EntityRequest): - "Utility class for PowerFlex" - def __init__(self, token, configuration): - super().__init__(token, configuration) - - def get_statistics_for_all_storagepools(self, ids=None, properties=None): - """list storagepool statistics for PowerFlex. - - :param ids: list - :param properties: list - :return: dict - """ - - action = 'querySelectedStatistics' - version = self.get_api_version() - default_properties = StoragePoolConstants.DEFAULT_STATISTICS_PROPERTIES - if version != '3.5': - default_properties = default_properties + \ - StoragePoolConstants.DEFAULT_STATISTICS_PROPERTIES_ABOVE_3_5 - params = { - 'properties': default_properties if properties is None else properties} - if ids is None: - params['allIds'] = "" - else: - params['ids'] = ids - - r, response = self.send_post_request(self.list_statistics_url, - entity='StoragePool', - action=action, - params=params) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to list storage pool statistics for PowerFlex. " - f"Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return response - - def get_statistics_for_all_volumes(self, ids=None, properties=None): - """list volume statistics for PowerFlex. - - :param ids: list - :param properties: list - :return: dict - """ - - action = 'querySelectedStatistics' - - params = { - 'properties': ( - VolumeConstants.DEFAULT_STATISTICS_PROPERTIES - if properties is None - else properties - ) - } - if ids is None: - params['allIds'] = "" - else: - params['ids'] = ids - - r, response = self.send_post_request(self.list_statistics_url, - entity='Volume', - action=action, - params=params) - if r.status_code != requests.codes.ok: - msg = ( - 'Failed to list volume statistics for PowerFlex. ' - f'Error: {response}' - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return response - - def get_statistics_for_all_snapshot_policies( - self, ids=None, properties=None): - """list snapshot policy statistics for PowerFlex. - - :param ids: list - :param properties: list - :return: dict - """ - - action = 'querySelectedStatistics' - - params = {} - if properties is None: - params['properties'] = SnapshotPolicyConstants.DEFAULT_STATISTICS_PROPERTIES - else: - params['properties'] = properties - if ids is None: - params['allIds'] = "" - else: - params['ids'] = ids - - r, response = self.send_post_request(self.list_statistics_url, - entity='SnapshotPolicy', - action=action, - params=params) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to list snapshot policy statistics for PowerFlex. " - f"Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return response diff --git a/PyPowerFlex/gen2/objects/volume.py b/PyPowerFlex/gen2/objects/volume.py deleted file mode 100644 index c160f19..0000000 --- a/PyPowerFlex/gen2/objects/volume.py +++ /dev/null @@ -1,574 +0,0 @@ -# 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.""" - -# 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 - -LOG = logging.getLogger(__name__) - - -class CompressionMethod: - """Volume compression methods.""" - - invalid = 'Invalid' - none = 'None' - normal = 'Normal' - - -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', 'management', 'snap_mobility', - 'ntnx']) - for vol_class in supported_vol_classes: - locals()[vol_class] = vol_class - - -class Volume(base_client.EntityRequest): - """ - A class representing Volume client. - """ - def add_mapped_sdc(self, - volume_id, - sdc_id=None, - sdc_guid=None, - allow_multiple_mappings=None, - allow_ext_managed=None, - access_mode=None, - volume_class=VolumeClass.defaultclass): - """Map PowerFlex volume to SDC. - - :param volume_id: str - :param sdc_id: str - :param sdc_guid: str - :param allow_multiple_mappings: bool - :param allow_ext_managed: bool - :type access_mode: str - :param volume_class: str - :return: dict - """ - - action = 'addMappedSdc' - - if all([sdc_id, sdc_guid]) or not any([sdc_id, sdc_guid]): - msg = 'Either sdc_id or sdc_guid must be set.' - raise exceptions.InvalidInput(msg) - params = { - "sdcId": sdc_id, - "guid": sdc_guid, - "allowMultipleMappings": allow_multiple_mappings, - "allowOnExtManagedVol": allow_ext_managed, - "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 SDC. Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return self.get(entity_id=volume_id) - - def create(self, - storage_pool_id, - size_in_gb, - name=None, - volume_type=None, - use_rmcache=None, - compression_method=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 compression_method: one of predefined attributes of - CompressionMethod - :type compression_method: str - :param volume_class: str - :return: dict - """ - - params = { - 'storagePoolId': storage_pool_id, - 'volumeSizeInGb': size_in_gb, - 'name': name, - 'volumeType': volume_type, - 'useRmcache': use_rmcache, - 'compressionMethod': compression_method, - 'volumeClass': volume_class - } - - return self._create_entity(params) - - def delete(self, volume_id, remove_mode, allow_ext_managed=None, - volume_class=VolumeClass.defaultclass): - """Remove PowerFlex volume. - - :param volume_id: str - :param remove_mode: one of predefined attributes of RemoveMode - :param allow_ext_managed: bool - :param volume_class: str - :return: None - """ - - params = { - "removeMode": remove_mode, - "allowOnExtManagedVol": allow_ext_managed, - "volumeClass": volume_class - } - - return self._delete_entity(volume_id, params) - - def extend(self, volume_id, size_in_gb, allow_ext_managed=None, - volume_class=VolumeClass.defaultclass): - """Extend PowerFlex volume. - - :param volume_id: str - :param size_in_gb: int - :param allow_ext_managed: bool - :param volume_class: str - :return: dict - """ - - action = 'setVolumeSize' - - params = {"sizeInGB": size_in_gb, - "allowOnExtManagedVol": allow_ext_managed, - "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) - - return self.get(entity_id=volume_id) - - def get_statistics(self, volume_id, fields=None): - """Get related PowerFlex Statistics for volume. - - :type volume_id: str - :type fields: list|tuple - :rtype: dict - """ - - return self.get_related(volume_id, - 'Statistics', - fields) - - def lock_auto_snapshot(self, volume_id): - """Lock auto snapshot of PowerFlex volume. - - :param volume_id: str - :return: dict - """ - - action = 'lockAutoSnapshot' - - r, response = self.send_post_request(self.base_action_url, - action=action, - entity=self.entity, - entity_id=volume_id) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to lock AutoSnapshot for PowerFlex {self.entity} " - f"with id {volume_id}. " - f"Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return self.get(entity_id=volume_id) - - def remove_mapped_sdc(self, - volume_id, - sdc_id=None, - sdc_guid=None, - all_sdcs=None, - skip_appliance_validation=None, - allow_ext_managed=None, - volume_class=VolumeClass.defaultclass): - """Unmap PowerFlex volume from SDC. - - :param volume_id: str - :param sdc_id: str - :param sdc_guid: str - :param all_sdcs: bool - :param skip_appliance_validation: bool - :param allow_ext_managed: bool - :param volume_class: str - :return: dict - """ - - action = 'removeMappedSdc' - - if ( - all([sdc_id, sdc_guid, all_sdcs]) or - not any([sdc_id, sdc_guid, all_sdcs]) - ): - msg = 'Either sdc_id or sdc_guid or all_sdcs must be set.' - raise exceptions.InvalidInput(msg) - - params = { - "sdcId": sdc_id, - "guid": sdc_guid, - "allSdcs": all_sdcs, - "skipApplianceValidation": skip_appliance_validation, - "allowOnExtManagedVol": allow_ext_managed, - "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"SDC. Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return self.get(entity_id=volume_id) - - def rename(self, volume_id, name, allow_ext_managed=None, - volume_class=VolumeClass.defaultclass): - """Rename PowerFlex volume. - - :param volume_id: str - :param name: str - :param allow_ext_managed: bool - :param volume_class: str - :return: dict - """ - - action = 'setVolumeName' - - params = { - "newName": name, - "allowOnExtManagedVol": allow_ext_managed, - "volumeClass": volume_class - } - - return self._rename_entity(action, volume_id, params) - - def unlock_auto_snapshot(self, volume_id, remove_auto_snapshot=None): - """Unlock auto snapshot of PowerFlex volume. - - :param volume_id: str - :param remove_auto_snapshot: bool - :return: dict - """ - - action = 'unlockAutoSnapshot' - - params = { - "autoSnapshotWillBeRemoved": remove_auto_snapshot - } - - 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 unlock AutoSnapshot for PowerFlex {self.entity} " - f"with id {volume_id}. Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return self.get(entity_id=volume_id) - - 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) - - return self.get(entity_id=volume_id) - - def set_compression_method(self, volume_id, compression_method): - """ - Modify the compression method to be used for a Volume, relevant only - if the volume has a space efficient data layout. - - :param volume_id: ID of the volume - :type volume_id: str - :param compression_method: one of predefined attributes of - CompressionMethod - :type compression_method: str - :return: dict - """ - - action = 'modifyCompressionMethod' - - params = { - 'compressionMethod': compression_method, - } - - 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 compression method of PowerFlex " - f"{self.entity} with id {volume_id}. Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return self.get(entity_id=volume_id) - - def set_use_rmcache(self, volume_id, use_rmcache): - """ - Control the use of Read RAM Cache in the specified volume. - If you want to ensure that all I/O operations for this volume are - cached, the relevant Storage Pool should be configured to use cache, - and the relevant SDSs should all have caching enabled. - - :param volume_id: ID of the volume - :type volume_id: str - :param use_rmcache: Whether to use Read RAM cache or not - :type use_rm_cache: bool - :return: dict - """ - - action = 'setVolumeUseRmcache' - - params = { - "useRmcache": use_rmcache - } - - 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 use_rmcache of PowerFlex " - f"{self.entity} with id {volume_id}. " - f"Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return self.get(entity_id=volume_id) - - 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) - - return self.get(entity_id=volume_id) - - 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) - - return self.get(entity_id=snap_id) - - def set_volume_access_mode_limit(self, volume_id, access_mode_limit): - """ - Set the highest mapping access mode allowed for a volume. - - :param volume_id: ID of the volume - :type volume_id: str - :param access_mode_limit: Define the access mode limit of a volume, - options are ReadWrite or ReadOnly - :type access_mode_limit: str - :return: dict - """ - - action = 'setVolumeAccessModeLimit' - - params = {"accessModeLimit": access_mode_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 Volume Access Mode Limit of PowerFlex " - f"{self.entity} with id {volume_id}. Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return self.get(entity_id=volume_id) - - def query_selected_statistics(self, properties, ids=None): - """Query PowerFlex volume statistics. - - :type properties: list - :type ids: list of volume IDs or None for all volumes - :rtype: dict - """ - - action = "querySelectedStatistics" - - params = {'properties': properties} - - if ids: - params["ids"] = ids - else: - params["allIds"] = "" - - return self._query_selected_statistics(action, params) diff --git a/PyPowerFlex/gen2/objects/__init__.py b/PyPowerFlex/objects/common/__init__.py similarity index 66% rename from PyPowerFlex/gen2/objects/__init__.py rename to PyPowerFlex/objects/common/__init__.py index 05d8b69..61bc368 100644 --- a/PyPowerFlex/gen2/objects/__init__.py +++ b/PyPowerFlex/objects/common/__init__.py @@ -15,18 +15,14 @@ """This module contains the objects for interacting with the PowerFlex APIs.""" -from PyPowerFlex.gen2.objects.sdc import Sdc -from PyPowerFlex.gen2.objects.sdt import Sdt -from PyPowerFlex.gen2.objects.storage_node import StorageNode -from PyPowerFlex.gen2.objects.system import System -from PyPowerFlex.gen2.objects.volume import Volume -from PyPowerFlex.gen2.objects.utility import PowerFlexUtility +from PyPowerFlex.objects.common.system import System +from PyPowerFlex.objects.common.host import Host +from PyPowerFlex.objects.common.sdc import Sdc +from PyPowerFlex.objects.common.sdt import Sdt __all__ = [ 'Sdc', 'Sdt', - 'StorageNode', 'System', - 'Volume', - 'PowerFlexUtility', + 'Host', ] diff --git a/PyPowerFlex/gen1/objects/host.py b/PyPowerFlex/objects/common/host.py similarity index 100% rename from PyPowerFlex/gen1/objects/host.py rename to PyPowerFlex/objects/common/host.py diff --git a/PyPowerFlex/gen1/objects/sdc.py b/PyPowerFlex/objects/common/sdc.py similarity index 100% rename from PyPowerFlex/gen1/objects/sdc.py rename to PyPowerFlex/objects/common/sdc.py diff --git a/PyPowerFlex/gen1/objects/sdt.py b/PyPowerFlex/objects/common/sdt.py similarity index 100% rename from PyPowerFlex/gen1/objects/sdt.py rename to PyPowerFlex/objects/common/sdt.py diff --git a/PyPowerFlex/gen1/objects/system.py b/PyPowerFlex/objects/common/system.py similarity index 100% rename from PyPowerFlex/gen1/objects/system.py rename to PyPowerFlex/objects/common/system.py diff --git a/PyPowerFlex/objects/gen1/__init__.py b/PyPowerFlex/objects/gen1/__init__.py new file mode 100644 index 0000000..c9076ce --- /dev/null +++ b/PyPowerFlex/objects/gen1/__init__.py @@ -0,0 +1,51 @@ +# Copyright (c) 2024 Dell Inc. or its subsidiaries. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""This module contains the objects for interacting with the PowerFlex APIs.""" + +from PyPowerFlex.objects.gen1.device import Device +from PyPowerFlex.objects.gen1.fault_set import FaultSet +from PyPowerFlex.objects.gen1.protection_domain import ProtectionDomain +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.acceleration_pool import AccelerationPool +from PyPowerFlex.objects.gen1.volume import Volume +from PyPowerFlex.objects.gen1.utility import PowerFlexUtility +from PyPowerFlex.objects.gen1.replication_consistency_group import ReplicationConsistencyGroup +from PyPowerFlex.objects.gen1.replication_pair import ReplicationPair +from PyPowerFlex.objects.gen1.service_template import ServiceTemplate +from PyPowerFlex.objects.gen1.managed_device import ManagedDevice +from PyPowerFlex.objects.gen1.deployment import Deployment +from PyPowerFlex.objects.gen1.firmware_repository import FirmwareRepository + + +__all__ = [ + 'Device', + 'FaultSet', + 'ProtectionDomain', + 'Sds', + 'SnapshotPolicy', + 'StoragePool', + 'AccelerationPool', + 'Volume', + 'PowerFlexUtility', + 'ReplicationConsistencyGroup', + 'ReplicationPair', + 'ServiceTemplate', + 'ManagedDevice', + 'Deployment', + 'FirmwareRepository', +] diff --git a/PyPowerFlex/gen1/objects/acceleration_pool.py b/PyPowerFlex/objects/gen1/acceleration_pool.py similarity index 100% rename from PyPowerFlex/gen1/objects/acceleration_pool.py rename to PyPowerFlex/objects/gen1/acceleration_pool.py diff --git a/PyPowerFlex/gen1/objects/deployment.py b/PyPowerFlex/objects/gen1/deployment.py similarity index 100% rename from PyPowerFlex/gen1/objects/deployment.py rename to PyPowerFlex/objects/gen1/deployment.py diff --git a/PyPowerFlex/gen1/objects/device.py b/PyPowerFlex/objects/gen1/device.py similarity index 100% rename from PyPowerFlex/gen1/objects/device.py rename to PyPowerFlex/objects/gen1/device.py diff --git a/PyPowerFlex/gen1/objects/fault_set.py b/PyPowerFlex/objects/gen1/fault_set.py similarity index 100% rename from PyPowerFlex/gen1/objects/fault_set.py rename to PyPowerFlex/objects/gen1/fault_set.py diff --git a/PyPowerFlex/gen1/objects/firmware_repository.py b/PyPowerFlex/objects/gen1/firmware_repository.py similarity index 100% rename from PyPowerFlex/gen1/objects/firmware_repository.py rename to PyPowerFlex/objects/gen1/firmware_repository.py diff --git a/PyPowerFlex/gen1/objects/managed_device.py b/PyPowerFlex/objects/gen1/managed_device.py similarity index 100% rename from PyPowerFlex/gen1/objects/managed_device.py rename to PyPowerFlex/objects/gen1/managed_device.py diff --git a/PyPowerFlex/gen1/objects/protection_domain.py b/PyPowerFlex/objects/gen1/protection_domain.py similarity index 100% rename from PyPowerFlex/gen1/objects/protection_domain.py rename to PyPowerFlex/objects/gen1/protection_domain.py diff --git a/PyPowerFlex/gen1/objects/replication_consistency_group.py b/PyPowerFlex/objects/gen1/replication_consistency_group.py similarity index 100% rename from PyPowerFlex/gen1/objects/replication_consistency_group.py rename to PyPowerFlex/objects/gen1/replication_consistency_group.py diff --git a/PyPowerFlex/gen1/objects/replication_pair.py b/PyPowerFlex/objects/gen1/replication_pair.py similarity index 100% rename from PyPowerFlex/gen1/objects/replication_pair.py rename to PyPowerFlex/objects/gen1/replication_pair.py diff --git a/PyPowerFlex/gen1/objects/sds.py b/PyPowerFlex/objects/gen1/sds.py similarity index 100% rename from PyPowerFlex/gen1/objects/sds.py rename to PyPowerFlex/objects/gen1/sds.py diff --git a/PyPowerFlex/gen1/objects/service_template.py b/PyPowerFlex/objects/gen1/service_template.py similarity index 100% rename from PyPowerFlex/gen1/objects/service_template.py rename to PyPowerFlex/objects/gen1/service_template.py diff --git a/PyPowerFlex/gen1/objects/snapshot_policy.py b/PyPowerFlex/objects/gen1/snapshot_policy.py similarity index 100% rename from PyPowerFlex/gen1/objects/snapshot_policy.py rename to PyPowerFlex/objects/gen1/snapshot_policy.py diff --git a/PyPowerFlex/gen1/objects/storage_pool.py b/PyPowerFlex/objects/gen1/storage_pool.py similarity index 99% rename from PyPowerFlex/gen1/objects/storage_pool.py rename to PyPowerFlex/objects/gen1/storage_pool.py index 2602e9c..54175cf 100644 --- a/PyPowerFlex/gen1/objects/storage_pool.py +++ b/PyPowerFlex/objects/gen1/storage_pool.py @@ -23,7 +23,7 @@ from PyPowerFlex import base_client from PyPowerFlex import exceptions -from PyPowerFlex.gen1.objects import Sds +from PyPowerFlex.objects.gen1 import Sds LOG = logging.getLogger(__name__) diff --git a/PyPowerFlex/gen1/objects/utility.py b/PyPowerFlex/objects/gen1/utility.py similarity index 100% rename from PyPowerFlex/gen1/objects/utility.py rename to PyPowerFlex/objects/gen1/utility.py diff --git a/PyPowerFlex/gen1/objects/volume.py b/PyPowerFlex/objects/gen1/volume.py similarity index 100% rename from PyPowerFlex/gen1/objects/volume.py rename to PyPowerFlex/objects/gen1/volume.py diff --git a/PyPowerFlex/objects/gen2/__init__.py b/PyPowerFlex/objects/gen2/__init__.py new file mode 100644 index 0000000..efa7737 --- /dev/null +++ b/PyPowerFlex/objects/gen2/__init__.py @@ -0,0 +1,24 @@ +# 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. + +"""This module contains the objects for interacting with the PowerFlex 5.0+ APIs.""" + +from PyPowerFlex.objects.gen2.storage_node import StorageNode +from PyPowerFlex.objects.gen2.utility import PowerFlexUtility + +__all__ = [ + 'StorageNode', + 'PowerFlexUtility', +] diff --git a/PyPowerFlex/gen2/objects/storage_node.py b/PyPowerFlex/objects/gen2/storage_node.py similarity index 100% rename from PyPowerFlex/gen2/objects/storage_node.py rename to PyPowerFlex/objects/gen2/storage_node.py diff --git a/PyPowerFlex/objects/gen2/utility.py b/PyPowerFlex/objects/gen2/utility.py new file mode 100644 index 0000000..c03fa10 --- /dev/null +++ b/PyPowerFlex/objects/gen2/utility.py @@ -0,0 +1,69 @@ +# 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. + +"""Utility module for PowerFlex.""" + +# pylint: disable=no-member,useless-parent-delegation +import logging + +import requests + +from PyPowerFlex import base_client +from PyPowerFlex import exceptions +from PyPowerFlex.constants import StoragePoolConstants, VolumeConstants, SnapshotPolicyConstants + + +LOG = logging.getLogger(__name__) + + +class PowerFlexUtility(base_client.EntityRequest): + "Utility class for PowerFlex" + def __init__(self, token, configuration): + super().__init__(token, configuration) + + # def get_statistics_for_all_storagepools(self, ids=None, properties=None): + # """list storagepool statistics for PowerFlex 5.0+. + + # :param ids: list + # :param properties: list + # :return: dict + # """ + + # action = 'querySelectedStatistics' + # version = self.get_api_version() + # default_properties = StoragePoolConstants.DEFAULT_STATISTICS_PROPERTIES + # if version != '3.5': + # default_properties = default_properties + \ + # StoragePoolConstants.DEFAULT_STATISTICS_PROPERTIES_ABOVE_3_5 + # params = { + # 'properties': default_properties if properties is None else properties} + # if ids is None: + # params['allIds'] = "" + # else: + # params['ids'] = ids + + # r, response = self.send_post_request(self.metrics_query_url, + # entity='StoragePool', + # action=action, + # params=params) + # if r.status_code != requests.codes.ok: + # msg = ( + # f"Failed to list storage pool statistics for PowerFlex. " + # f"Error: {response}" + # ) + # LOG.error(msg) + # raise exceptions.PowerFlexClientException(msg) + + # return response diff --git a/setup.py b/setup.py index 7a91809..065bca6 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ # pylint: disable=import-error -from setuptools import setup, find_packages +from setuptools import setup setup( name='PyPowerFlex', @@ -33,8 +33,8 @@ classifiers=['License :: OSI Approved :: Apache Software License'], packages=[ 'PyPowerFlex', - 'PyPowerFlex.gen1', - 'PyPowerFlex.gen2', + 'PyPowerFlex.objects.gen1', + 'PyPowerFlex.objects.gen2', ], python_requires='>=3.5' ) diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/common/__init__.py b/tests/common/__init__.py new file mode 100644 index 0000000..7440036 --- /dev/null +++ b/tests/common/__init__.py @@ -0,0 +1,244 @@ +# 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. + +"""This module is used for the initialization of the test framework.""" + +# pylint: disable=too-many-instance-attributes,keyword-arg-before-vararg,broad-exception-raised,unused-argument + +import collections +import contextlib +import json +import logging +from unittest import mock +from unittest import TestCase + +import requests + +import PyPowerFlex +from PyPowerFlex import utils + + +class MockResponse(requests.Response): + """ + Mock HTTP Response. + + Defines http replies from mocked calls to do_request(). + """ + def __init__(self, content, status_code=200): + """ + Initialize a MockResponse. + + Args: + content (str or dict): The content of the response. + status_code (int): The status code of the response. + """ + super().__init__() + self._content = content + self.request = mock.MagicMock() + self.status_code = status_code + + def json(self, **kwargs): + """ + Return the content of the response as JSON. + + Args: + **kwargs: Additional keyword arguments. + + Returns: + dict: The content of the response. + """ + return self._content + + @property + def text(self): + """ + Return the content of the response as text. + + Returns: + str: The content of the response. + """ + if not isinstance(self._content, bytes): + return json.dumps(self._content) + return super().text + + +class PyPowerFlexTestCase(TestCase): + """ + Base test case for PyPowerFlex. + + Provides a mocked HTTP response for testing. + """ + VERSION_API_PATH = '/version' + + @classmethod + def version(cls, new_version): + def decorator(subclass): + cls.DEFAULT_MOCK_RESPONSES[ + PyPowerFlexTestCase.RESPONSE_MODE.Valid + ][cls.VERSION_API_PATH] = new_version + return subclass + return decorator + + RESPONSE_MODE = ( + collections.namedtuple('RESPONSE_MODE', 'Valid Invalid BadStatus') + (Valid='Valid', Invalid='Invalid', BadStatus='BadStatus') + ) + BAD_STATUS_RESPONSE = MockResponse( + { + 'errorCode': 500, + 'message': 'Test default bad status', + }, 500 + ) + MOCK_RESPONSES = {} + DEFAULT_MOCK_RESPONSES = { + RESPONSE_MODE.Valid: { + '/login': 'token', + VERSION_API_PATH: '3.5', + '/logout': '', + }, + RESPONSE_MODE.Invalid: { + VERSION_API_PATH: '2.5', + }, + RESPONSE_MODE.BadStatus: { + '/login': MockResponse( + { + 'errorCode': 1, + 'message': 'Test login bad status', + }, 400 + ), + '/version': MockResponse( + { + 'errorCode': 2, + 'message': 'Test version bad status', + }, 400 + ), + '/logout': MockResponse( + { + 'errorCode': 3, + 'message': 'Test logout bad status', + }, 400 + ) + } + } + __http_response_mode = RESPONSE_MODE.Valid + + def setUp(self): + """ + Set up the test case. + + Creates a PyPowerFlex client and sets up mocked HTTP responses. + """ + self.gateway_address = '1.2.3.4' + self.gateway_port = 443 + self.username = 'admin' + self.password = 'admin' + self.client = PyPowerFlex.PowerFlexClient(self.gateway_address, + self.gateway_port, + self.username, + self.password, + log_level=logging.DEBUG) + requests.request = self.get_mock_response + self.get_mock = self.mock_object(requests, + 'get', + side_effect=self.get_mock_response) + self.post_mock = self.mock_object(requests, + 'post', + side_effect=self.get_mock_response) + utils.is_version_3 = mock.MagicMock(return_value=True) + + def mock_object(self, obj, attr_name, *args, **kwargs): + """Use python mock to mock an object attribute. + + Mocks the specified objects attribute with the given value. + Automatically performs 'addCleanup' for the mock. + """ + patcher = mock.patch.object(obj, attr_name, *args, **kwargs) + result = patcher.start() + self.addCleanup(patcher.stop) + return result + + @contextlib.contextmanager + def http_response_mode(self, mode): + """ + Context manager for setting the HTTP response mode. + + Args: + mode: The HTTP response mode. + + Yields: + None. + """ + previous_response_mode, self.__http_response_mode = ( + self.__http_response_mode, mode + ) + yield + self.__http_response_mode = previous_response_mode + + def get_mock_response(self, url, request_url=None, mode=None, *args, **kwargs): + """ + Get a mock HTTP response. + + Args: + url (str): The URL of the request. + request_url (str): The URL of the request. + mode (str): The HTTP response mode. + *args: Additional arguments. + **kwargs: Additional keyword arguments. + + Returns: + requests.Response: The mocked HTTP response. + """ + if mode is None: + mode = self.__http_response_mode + + api_path = url.split('/api')[1] if ('/api' in url) else request_url.split('/api')[1] + try: + if api_path == "/login": + response = self.RESPONSE_MODE.Valid[0] + elif api_path == "/logout": + response = self.RESPONSE_MODE.Valid[2] + else: + response = self.MOCK_RESPONSES[mode][api_path] + except KeyError as e: + try: + response = self.DEFAULT_MOCK_RESPONSES[mode][api_path] + except KeyError: + if mode == self.RESPONSE_MODE.BadStatus: + response = self.BAD_STATUS_RESPONSE + else: + raise Exception( + f"Mock API Endpoint is not implemented: [{mode}]" + f"{api_path}" + ) from e + if not isinstance(response, MockResponse): + response = self._get_mock_response(response) + + response.request.url = url + response.request.body = kwargs.get('data') + return response + + def _get_mock_response(self, response): + """ + Returns a MockResponse object based on the given response. + + Args: + response (str): The response to be wrapped. + + Returns: + MockResponse: The mock response object. + """ + if "204" in str(response): + return MockResponse(response, 204) + return MockResponse(response, 200) diff --git a/tests/gen1/test_host.py b/tests/common/test_host.py similarity index 100% rename from tests/gen1/test_host.py rename to tests/common/test_host.py diff --git a/tests/gen1/test_sdc.py b/tests/common/test_sdc.py similarity index 100% rename from tests/gen1/test_sdc.py rename to tests/common/test_sdc.py diff --git a/tests/gen1/test_sdt.py b/tests/common/test_sdt.py similarity index 99% rename from tests/gen1/test_sdt.py rename to tests/common/test_sdt.py index daf1fe3..734be89 100644 --- a/tests/gen1/test_sdt.py +++ b/tests/common/test_sdt.py @@ -18,7 +18,7 @@ # pylint: disable=invalid-name,too-many-public-methods from PyPowerFlex import exceptions -from PyPowerFlex.gen1.objects import sdt +from PyPowerFlex.objects.common import sdt from tests.gen1 import PyPowerFlexTestCase diff --git a/tests/gen1/test_system.py b/tests/common/test_system.py similarity index 99% rename from tests/gen1/test_system.py rename to tests/common/test_system.py index 5ce9330..ed64a97 100644 --- a/tests/gen1/test_system.py +++ b/tests/common/test_system.py @@ -18,7 +18,7 @@ # pylint: disable=invalid-name,too-many-public-methods,duplicate-code from PyPowerFlex import exceptions -from PyPowerFlex.gen1.objects import system +from PyPowerFlex.objects.common import system from tests.gen1 import PyPowerFlexTestCase diff --git a/tests/gen1/test_acceleration_pool.py b/tests/gen1/test_acceleration_pool.py index 0d253fc..d1fe4fe 100644 --- a/tests/gen1/test_acceleration_pool.py +++ b/tests/gen1/test_acceleration_pool.py @@ -18,8 +18,8 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -from PyPowerFlex.gen1.objects import acceleration_pool -from tests.gen1 import PyPowerFlexTestCase +from PyPowerFlex.objects.gen1 import acceleration_pool +from tests.common import PyPowerFlexTestCase class TestAccelerationPoolClient(PyPowerFlexTestCase): diff --git a/tests/gen1/test_deployment.py b/tests/gen1/test_deployment.py index 2dfb75d..b782dbb 100644 --- a/tests/gen1/test_deployment.py +++ b/tests/gen1/test_deployment.py @@ -18,7 +18,7 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -from tests.gen1 import PyPowerFlexTestCase +from tests.common import PyPowerFlexTestCase class TestDeploymentClient(PyPowerFlexTestCase): """ diff --git a/tests/gen1/test_device.py b/tests/gen1/test_device.py index d80a327..96ea451 100644 --- a/tests/gen1/test_device.py +++ b/tests/gen1/test_device.py @@ -18,8 +18,8 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -from PyPowerFlex.gen1.objects.device import MediaType -from tests.gen1 import PyPowerFlexTestCase +from PyPowerFlex.objects.gen1.device import MediaType +from tests.common import PyPowerFlexTestCase class TestDeviceClient(PyPowerFlexTestCase): diff --git a/tests/gen1/test_fault_set.py b/tests/gen1/test_fault_set.py index 89b215e..b9a56d6 100644 --- a/tests/gen1/test_fault_set.py +++ b/tests/gen1/test_fault_set.py @@ -18,7 +18,7 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -from tests.gen1 import PyPowerFlexTestCase +from tests.common import PyPowerFlexTestCase class TestFaultSetClient(PyPowerFlexTestCase): diff --git a/tests/gen1/test_firmware_repository.py b/tests/gen1/test_firmware_repository.py index 475adba..5a8d6b7 100644 --- a/tests/gen1/test_firmware_repository.py +++ b/tests/gen1/test_firmware_repository.py @@ -18,7 +18,7 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -from tests.gen1 import PyPowerFlexTestCase +from tests.common import PyPowerFlexTestCase class TestFirmwareRepositoryClient(PyPowerFlexTestCase): diff --git a/tests/gen1/test_managed_device.py b/tests/gen1/test_managed_device.py index 5a724a5..bd7f056 100644 --- a/tests/gen1/test_managed_device.py +++ b/tests/gen1/test_managed_device.py @@ -18,7 +18,7 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -from tests.gen1 import PyPowerFlexTestCase +from tests.common import PyPowerFlexTestCase class TestManagedDeviceClient(PyPowerFlexTestCase): diff --git a/tests/gen1/test_protection_domain.py b/tests/gen1/test_protection_domain.py index 74fef7d..63c529b 100644 --- a/tests/gen1/test_protection_domain.py +++ b/tests/gen1/test_protection_domain.py @@ -18,8 +18,8 @@ # pylint: disable=invalid-name,too-many-public-methods,duplicate-code from PyPowerFlex import exceptions -from PyPowerFlex.gen1.objects import protection_domain -from tests.gen1 import PyPowerFlexTestCase +from PyPowerFlex.objects.gen1 import protection_domain +from tests.common import PyPowerFlexTestCase class TestProtectionDomainClient(PyPowerFlexTestCase): diff --git a/tests/gen1/test_replication_consistency_group.py b/tests/gen1/test_replication_consistency_group.py index d9a737a..40c4a10 100644 --- a/tests/gen1/test_replication_consistency_group.py +++ b/tests/gen1/test_replication_consistency_group.py @@ -18,7 +18,7 @@ # pylint: disable=invalid-name,too-many-public-methods from PyPowerFlex import exceptions -from tests.gen1 import PyPowerFlexTestCase +from tests.common import PyPowerFlexTestCase class TestReplicationConsistencyGroupClient(PyPowerFlexTestCase): diff --git a/tests/gen1/test_replication_pair.py b/tests/gen1/test_replication_pair.py index e20af7a..517e9c3 100644 --- a/tests/gen1/test_replication_pair.py +++ b/tests/gen1/test_replication_pair.py @@ -18,7 +18,7 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -from tests.gen1 import PyPowerFlexTestCase +from tests.common import PyPowerFlexTestCase class TestReplicationPairClient(PyPowerFlexTestCase): diff --git a/tests/gen1/test_sds.py b/tests/gen1/test_sds.py index 032aa89..76f2502 100644 --- a/tests/gen1/test_sds.py +++ b/tests/gen1/test_sds.py @@ -18,8 +18,8 @@ # pylint: disable=invalid-name,too-many-public-methods from PyPowerFlex import exceptions -from PyPowerFlex.gen1.objects import sds -from tests.gen1 import PyPowerFlexTestCase +from PyPowerFlex.objects.gen1 import sds +from tests.common import PyPowerFlexTestCase class TestSdsClient(PyPowerFlexTestCase): diff --git a/tests/gen1/test_service_template.py b/tests/gen1/test_service_template.py index 197cfa1..11bd326 100644 --- a/tests/gen1/test_service_template.py +++ b/tests/gen1/test_service_template.py @@ -18,7 +18,7 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -from tests.gen1 import PyPowerFlexTestCase +from tests.common import PyPowerFlexTestCase class TestServiceTemplateClient(PyPowerFlexTestCase): diff --git a/tests/gen1/test_snapshot_policy.py b/tests/gen1/test_snapshot_policy.py index 9927f7c..83617c7 100644 --- a/tests/gen1/test_snapshot_policy.py +++ b/tests/gen1/test_snapshot_policy.py @@ -18,8 +18,8 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -from PyPowerFlex.gen1.objects import snapshot_policy as sp -from tests.gen1 import PyPowerFlexTestCase +from PyPowerFlex.objects.gen1 import snapshot_policy as sp +from tests.common import PyPowerFlexTestCase class TestSnapshotPolicyClient(PyPowerFlexTestCase): diff --git a/tests/gen1/test_storage_pool.py b/tests/gen1/test_storage_pool.py index 640b72d..5b8e3bf 100644 --- a/tests/gen1/test_storage_pool.py +++ b/tests/gen1/test_storage_pool.py @@ -18,10 +18,10 @@ # pylint: disable=invalid-name,too-many-public-methods from PyPowerFlex import exceptions -from PyPowerFlex.gen1.objects.storage_pool import CompressionMethod -from PyPowerFlex.gen1.objects.storage_pool import ExternalAccelerationType -from PyPowerFlex.gen1.objects.storage_pool import MediaType -from tests.gen1 import PyPowerFlexTestCase +from PyPowerFlex.objects.gen1.storage_pool import CompressionMethod +from PyPowerFlex.objects.gen1.storage_pool import ExternalAccelerationType +from PyPowerFlex.objects.gen1.storage_pool import MediaType +from tests.common import PyPowerFlexTestCase class TestStoragePoolClient(PyPowerFlexTestCase): diff --git a/tests/gen1/test_utility.py b/tests/gen1/test_utility.py index eabedff..b26d72d 100644 --- a/tests/gen1/test_utility.py +++ b/tests/gen1/test_utility.py @@ -18,7 +18,7 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -from tests.gen1 import PyPowerFlexTestCase +from tests.common import PyPowerFlexTestCase class TestPowerFlexUtility(PyPowerFlexTestCase): diff --git a/tests/gen1/test_volume.py b/tests/gen1/test_volume.py index 2dcd9a2..ef18f0c 100644 --- a/tests/gen1/test_volume.py +++ b/tests/gen1/test_volume.py @@ -18,9 +18,8 @@ # pylint: disable=invalid-name,too-many-public-methods from PyPowerFlex import exceptions -from PyPowerFlex.gen1.objects import volume -# import tests -from tests.gen1 import PyPowerFlexTestCase +from PyPowerFlex.objects.gen1 import volume +from tests.common import PyPowerFlexTestCase class TestVolumeClient(PyPowerFlexTestCase): """ diff --git a/tests/gen2/__init__.py b/tests/gen2/__init__.py index e69de29..e0795f7 100644 --- a/tests/gen2/__init__.py +++ b/tests/gen2/__init__.py @@ -0,0 +1,235 @@ +# 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. + +"""This module is used for the initialization of the test framework.""" + +# pylint: disable=too-many-instance-attributes,keyword-arg-before-vararg,broad-exception-raised,unused-argument + +import collections +import contextlib +import json +import logging +from unittest import mock +from unittest import TestCase + +import requests + +import PyPowerFlex +from PyPowerFlex import utils + + +class MockResponse(requests.Response): + """ + Mock HTTP Response. + + Defines http replies from mocked calls to do_request(). + """ + + def __init__(self, content, status_code=200): + """ + Initialize a MockResponse. + + Args: + content (str or dict): The content of the response. + status_code (int): The status code of the response. + """ + super().__init__() + self._content = content + self.request = mock.MagicMock() + self.status_code = status_code + + def json(self, **kwargs): + """ + Return the content of the response as JSON. + + Args: + **kwargs: Additional keyword arguments. + + Returns: + dict: The content of the response. + """ + return self._content + + @property + def text(self): + """ + Return the content of the response as text. + + Returns: + str: The content of the response. + """ + if not isinstance(self._content, bytes): + return json.dumps(self._content) + return super().text + + +class PyPowerFlexTestCase(TestCase): + """ + Base test case for PyPowerFlex. + + Provides a mocked HTTP response for testing. + """ + RESPONSE_MODE = ( + collections.namedtuple('RESPONSE_MODE', 'Valid Invalid BadStatus') + (Valid='Valid', Invalid='Invalid', BadStatus='BadStatus') + ) + BAD_STATUS_RESPONSE = MockResponse( + { + 'errorCode': 500, + 'message': 'Test default bad status', + }, 500 + ) + MOCK_RESPONSES = {} + DEFAULT_MOCK_RESPONSES = { + RESPONSE_MODE.Valid: { + '/login': 'token', + '/version': '5.0', + '/logout': '', + }, + RESPONSE_MODE.Invalid: { + '/version': '2.5', + }, + RESPONSE_MODE.BadStatus: { + '/login': MockResponse( + { + 'errorCode': 1, + 'message': 'Test login bad status', + }, 400 + ), + '/version': MockResponse( + { + 'errorCode': 2, + 'message': 'Test version bad status', + }, 400 + ), + '/logout': MockResponse( + { + 'errorCode': 3, + 'message': 'Test logout bad status', + }, 400 + ) + } + } + __http_response_mode = RESPONSE_MODE.Valid + + def setUp(self): + """ + Set up the test case. + + Creates a PyPowerFlex client and sets up mocked HTTP responses. + """ + self.gateway_address = '1.2.3.4' + self.gateway_port = 443 + self.username = 'admin' + self.password = 'admin' + self.client = PyPowerFlex.PowerFlexClient(self.gateway_address, + self.gateway_port, + self.username, + self.password, + log_level=logging.DEBUG) + requests.request = self.get_mock_response + self.get_mock = self.mock_object(requests, + 'get', + side_effect=self.get_mock_response) + self.post_mock = self.mock_object(requests, + 'post', + side_effect=self.get_mock_response) + utils.is_version_3 = mock.MagicMock(return_value=True) + + def mock_object(self, obj, attr_name, *args, **kwargs): + """Use python mock to mock an object attribute. + + Mocks the specified objects attribute with the given value. + Automatically performs 'addCleanup' for the mock. + """ + patcher = mock.patch.object(obj, attr_name, *args, **kwargs) + result = patcher.start() + self.addCleanup(patcher.stop) + return result + + @contextlib.contextmanager + def http_response_mode(self, mode): + """ + Context manager for setting the HTTP response mode. + + Args: + mode: The HTTP response mode. + + Yields: + None. + """ + previous_response_mode, self.__http_response_mode = ( + self.__http_response_mode, mode + ) + yield + self.__http_response_mode = previous_response_mode + + def get_mock_response(self, url, request_url=None, mode=None, *args, **kwargs): + """ + Get a mock HTTP response. + + Args: + url (str): The URL of the request. + request_url (str): The URL of the request. + mode (str): The HTTP response mode. + *args: Additional arguments. + **kwargs: Additional keyword arguments. + + Returns: + requests.Response: The mocked HTTP response. + """ + if mode is None: + mode = self.__http_response_mode + + api_path = url.split( + '/api')[1] if ('/api' in url) else request_url.split('/api')[1] + try: + if api_path == "/login": + response = self.RESPONSE_MODE.Valid[0] + elif api_path == "/logout": + response = self.RESPONSE_MODE.Valid[2] + else: + response = self.MOCK_RESPONSES[mode][api_path] + except KeyError as e: + try: + response = self.DEFAULT_MOCK_RESPONSES[mode][api_path] + except KeyError: + if mode == self.RESPONSE_MODE.BadStatus: + response = self.BAD_STATUS_RESPONSE + else: + raise Exception( + f"Mock API Endpoint is not implemented: [{mode}]" + f"{api_path}" + ) from e + if not isinstance(response, MockResponse): + response = self._get_mock_response(response) + + response.request.url = url + response.request.body = kwargs.get('data') + return response + + def _get_mock_response(self, response): + """ + Returns a MockResponse object based on the given response. + + Args: + response (str): The response to be wrapped. + + Returns: + MockResponse: The mock response object. + """ + if "204" in str(response): + return MockResponse(response, 204) + return MockResponse(response, 200) diff --git a/tests/gen2/test_storage_node.py b/tests/gen2/test_storage_node.py index a5dc59d..087e4bf 100644 --- a/tests/gen2/test_storage_node.py +++ b/tests/gen2/test_storage_node.py @@ -18,10 +18,11 @@ # pylint: disable=invalid-name,too-many-public-methods from PyPowerFlex import exceptions -from PyPowerFlex.gen2.objects import storage_node -from tests.gen1 import PyPowerFlexTestCase +from PyPowerFlex.objects.gen2 import storage_node +from tests.common import PyPowerFlexTestCase +@PyPowerFlexTestCase.version('5.0') class TestStorageNodeClient(PyPowerFlexTestCase): """ Tests for the StorageNodeClient class. @@ -41,273 +42,265 @@ def setUp(self): self.MOCK_RESPONSES = { self.RESPONSE_MODE.Valid: { f'/types/{storage_node.StorageNode.entity}/instances': - {'id': self.fake_sds_id}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}': - {'id': self.fake_sds_id}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/addIp': + {'id': self.fake_node_id}, + f'/instances/{storage_node.StorageNode.entity}::{self.fake_node_id}': + {'id': self.fake_node_id}, + f'/instances/{storage_node.StorageNode.entity}::{self.fake_node_id}/action/addIp': {}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/removeSds': + f'/instances/{storage_node.StorageNode.entity}::{self.fake_node_id}/action/removestorage_node': {}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/relationships/Device': + f'/instances/{storage_node.StorageNode.entity}::{self.fake_node_id}/relationships/Device': [], - f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/setSdsName': + f'/instances/{storage_node.StorageNode.entity}::{self.fake_node_id}/action/setstorage_nodeName': {}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/removeSdsIp': + f'/instances/{storage_node.StorageNode.entity}::{self.fake_node_id}/action/removestorage_nodeIp': {}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/setSdsIpRole': + f'/instances/{storage_node.StorageNode.entity}::{self.fake_node_id}/action/setstorage_nodeIpRole': {}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/setSdsPort': + f'/instances/{storage_node.StorageNode.entity}::{self.fake_node_id}/action/setstorage_nodePort': {}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/enableRfcache': - {}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/disableRfcache': - {}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/setSdsRmcacheEnabled': - {}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/setSdsRmcacheSize': - {}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_sds_id}/action/setSdsPerformanceParameters': + f'/instances/{storage_node.StorageNode.entity}::{self.fake_node_id}/action/setstorage_nodePerformanceParameters': {}, f'/types/{storage_node.StorageNode.entity}' '/instances/action/querySelectedStatistics': { - self.fake_sds_id: {'rfcacheFdReadTimeGreater5Sec': 0} + self.fake_node_id: {'rfcacheFdReadTimeGreater5Sec': 0} }, }, self.RESPONSE_MODE.Invalid: { - '/types/Sds/instances': + f'/types/{storage_node.StorageNode.entity}/instances': {}, } } - def test_sds_add_ip(self): + def test_storage_node_add_ip(self): """ - Test the add_ip method of the SdsClient. + Test the add_ip method of the storage_node client. """ - self.client.sds.add_ip(self.fake_sds_id, self.fake_sds_ips[0]) + self.client.storage_node.add_ip(self.fake_node_id, self.fake_node_ips[0]) - def test_sds_add_ip_bad_status(self): + def test_storage_node_add_ip_bad_status(self): """ - Test the add_ip method of the SdsClient with a bad status. + Test the add_ip method of the storage_node client with a bad status. """ with self.http_response_mode(self.RESPONSE_MODE.BadStatus): self.assertRaises(exceptions.PowerFlexClientException, - self.client.sds.add_ip, - self.fake_sds_id, - self.fake_sds_ips[0]) + self.client.storage_node.add_ip, + self.fake_node_id, + self.fake_node_ips[0]) - def test_sds_create(self): + def test_storage_node_create(self): """ - Test the create method of the SdsClient. + Test the create method of the storage_node client. """ - self.client.sds.create(protection_domain_id=self.fake_pd_id, - sds_ips=self.fake_sds_ips) + self.client.storage_node.create(protection_domain_id=self.fake_pd_id, + storage_node_ips=self.fake_node_ips) - def test_sds_create_bad_status(self): + def test_storage_node_create_bad_status(self): """ - Test the create method of the SdsClient with a bad status. + Test the create method of the storage_node client with a bad status. """ with self.http_response_mode(self.RESPONSE_MODE.BadStatus): self.assertRaises(exceptions.PowerFlexFailCreating, - self.client.sds.create, + self.client.storage_node.create, protection_domain_id=self.fake_pd_id, - sds_ips=self.fake_sds_ips) + storage_node_ips=self.fake_node_ips) - def test_sds_create_no_id_in_response(self): + def test_storage_node_create_no_id_in_response(self): """ - Test the create method of the SdsClient with no ID in the response. + Test the create method of the storage_node client with no ID in the response. """ with self.http_response_mode(self.RESPONSE_MODE.Invalid): self.assertRaises(KeyError, - self.client.sds.create, + self.client.storage_node.create, protection_domain_id=self.fake_pd_id, - sds_ips=self.fake_sds_ips) + storage_node_ips=self.fake_node_ips) - def test_sds_delete(self): + def test_storage_node_delete(self): """ - Test the delete method of the SdsClient. + Test the delete method of the storage_node client. """ - self.client.sds.delete(self.fake_sds_id) + self.client.storage_node.delete(self.fake_node_id) - def test_sds_delete_bad_status(self): + def test_storage_node_delete_bad_status(self): """ - Test the delete method of the SdsClient with a bad status. + Test the delete method of the storage_node client with a bad status. """ with self.http_response_mode(self.RESPONSE_MODE.BadStatus): self.assertRaises(exceptions.PowerFlexFailDeleting, - self.client.sds.delete, - self.fake_sds_id) + self.client.storage_node.delete, + self.fake_node_id) - def test_sds_get_devices(self): + def test_storage_node_get_devices(self): """ - Test the get_devices method of the SdsClient. + Test the get_devices method of the storage_node client. """ - self.client.sds.get_devices(self.fake_sds_id) + self.client.storage_node.get_devices(self.fake_node_id) - def test_sds_get_devices_bad_status(self): + def test_storage_node_get_devices_bad_status(self): """ - Test the get_devices method of the SdsClient with a bad status. + Test the get_devices method of the storage_node client with a bad status. """ with self.http_response_mode(self.RESPONSE_MODE.BadStatus): self.assertRaises(exceptions.PowerFlexClientException, - self.client.sds.get_devices, - self.fake_sds_id) + self.client.storage_node.get_devices, + self.fake_node_id) - def test_sds_rename(self): + def test_storage_node_rename(self): """ - Test the rename method of the SdsClient. + Test the rename method of the storage_node client. """ - self.client.sds.rename(self.fake_sds_id, name='new_name') + self.client.storage_node.rename(self.fake_node_id, name='new_name') - def test_sds_rename_bad_status(self): + def test_storage_node_rename_bad_status(self): """ - Test the rename method of the SdsClient with a bad status. + Test the rename method of the storage_node client with a bad status. """ with self.http_response_mode(self.RESPONSE_MODE.BadStatus): self.assertRaises(exceptions.PowerFlexFailRenaming, - self.client.sds.rename, - self.fake_sds_id, + self.client.storage_node.rename, + self.fake_node_id, name='new_name') - def test_sds_remove_ip(self): + def test_storage_node_remove_ip(self): """ - Test the remove_ip method of the SdsClient. + Test the remove_ip method of the storage_node client. """ - self.client.sds.remove_ip(self.fake_sds_id, ip='1.2.3.4') + self.client.storage_node.remove_ip(self.fake_node_id, ip='1.2.3.4') - def test_sds_remove_ip_bad_status(self): + def test_storage_node_remove_ip_bad_status(self): """ - Test the remove_ip method of the SdsClient with a bad status. + Test the remove_ip method of the storage_node client with a bad status. """ with self.http_response_mode(self.RESPONSE_MODE.BadStatus): self.assertRaises(exceptions.PowerFlexClientException, - self.client.sds.remove_ip, - self.fake_sds_id, + self.client.storage_node.remove_ip, + self.fake_node_id, ip='1.2.3.4') - def test_sds_set_ip_role(self): + def test_storage_node_set_ip_role(self): """ Test the set_ip_role method. """ - self.client.sds.set_ip_role(self.fake_sds_id, + self.client.storage_node.set_ip_role(self.fake_node_id, ip='1.2.3.4', - role=sds.SdsIpRoles.sdc_only, + role=storage_node.storage_nodeIpRoles.sdc_only, force=True) - def test_sds_set_ip_role_bad_status(self): + def test_storage_node_set_ip_role_bad_status(self): """ Test the set_ip_role method with a bad status. """ with self.http_response_mode(self.RESPONSE_MODE.BadStatus): self.assertRaises(exceptions.PowerFlexClientException, - self.client.sds.set_ip_role, - self.fake_sds_id, + self.client.storage_node.set_ip_role, + self.fake_node_id, ip='1.2.3.4', - role=sds.SdsIpRoles.sdc_only, + role=storage_node.storage_nodeIpRoles.sdc_only, force=True) - def test_sds_set_port(self): + def test_storage_node_set_port(self): """ Test the set_port method. """ - self.client.sds.set_port(self.fake_sds_id, sds_port=4443) + self.client.storage_node.set_port(self.fake_node_id, storage_node_port=4443) - def test_sds_set_port_bad_status(self): + def test_storage_node_set_port_bad_status(self): """ Test the set_port method with a bad status. """ with self.http_response_mode(self.RESPONSE_MODE.BadStatus): self.assertRaises(exceptions.PowerFlexClientException, - self.client.sds.set_port, - self.fake_sds_id, - sds_port=4443) + self.client.storage_node.set_port, + self.fake_node_id, + storage_node_port=4443) - def test_sds_set_rfcache_enabled(self): + def test_storage_node_set_rfcache_enabled(self): """ Test the set_rfcache_enabled method. """ - self.client.sds.set_rfcache_enabled(self.fake_sds_id, + self.client.storage_node.set_rfcache_enabled(self.fake_node_id, rfcache_enabled=True) - def test_sds_set_rfcache_enabled_bad_status(self): + def test_storage_node_set_rfcache_enabled_bad_status(self): """ Test the set_rfcache_enabled method with a bad status. """ with self.http_response_mode(self.RESPONSE_MODE.BadStatus): self.assertRaises(exceptions.PowerFlexClientException, - self.client.sds.set_rfcache_enabled, - self.fake_sds_id, + self.client.storage_node.set_rfcache_enabled, + self.fake_node_id, rfcache_enabled=True) - def test_sds_set_rmcache_enabled(self): + def test_storage_node_set_rmcache_enabled(self): """ Test the set_rmcache_enabled method. """ - self.client.sds.set_rmcache_enabled(self.fake_sds_id, + self.client.storage_node.set_rmcache_enabled(self.fake_node_id, rmcache_enabled=True) - def test_sds_set_rmcache_enabled_bad_status(self): + def test_storage_node_set_rmcache_enabled_bad_status(self): """ Test the set_rmcache_enabled method with a bad status. """ with self.http_response_mode(self.RESPONSE_MODE.BadStatus): self.assertRaises(exceptions.PowerFlexClientException, - self.client.sds.set_rmcache_enabled, - self.fake_sds_id, + self.client.storage_node.set_rmcache_enabled, + self.fake_node_id, rmcache_enabled=True) - def test_sds_set_rmcache_size(self): + def test_storage_node_set_rmcache_size(self): """ Test the set_rmcache_size method. """ - self.client.sds.set_rmcache_size(self.fake_sds_id, + self.client.storage_node.set_rmcache_size(self.fake_node_id, rmcache_size=128) - def test_sds_set_rmcache_size_bad_status(self): + def test_storage_node_set_rmcache_size_bad_status(self): """ Test the set_rmcache_size method with a bad status. """ with self.http_response_mode(self.RESPONSE_MODE.BadStatus): self.assertRaises(exceptions.PowerFlexClientException, - self.client.sds.set_rmcache_size, - self.fake_sds_id, + self.client.storage_node.set_rmcache_size, + self.fake_node_id, rmcache_size=128) - def test_sds_set_performance_parameters(self): + def test_storage_node_set_performance_parameters(self): """ Test the set_performance_parameters method. """ - self.client.sds.set_performance_parameters( - self.fake_sds_id, - performance_profile=sds.PerformanceProfile.highperformance) + self.client.storage_node.set_performance_parameters( + self.fake_node_id, + performance_profile=storage_node.PerformanceProfile.highperformance) - def test_sds_set_performance_parameters_bad_status(self): + def test_storage_node_set_performance_parameters_bad_status(self): """ Test the set_performance_parameters method with a bad status. """ with self.http_response_mode(self.RESPONSE_MODE.BadStatus): self.assertRaises( exceptions.PowerFlexClientException, - self.client.sds.set_performance_parameters, - self.fake_sds_id, - performance_profile=sds.PerformanceProfile.highperformance) + self.client.storage_node.set_performance_parameters, + self.fake_node_id, + performance_profile=storage_node.PerformanceProfile.highperformance) - def test_sds_query_selected_statistics(self): + def test_storage_node_query_selected_statistics(self): """ Test the query_selected_statistics method. """ - ret = self.client.sds.query_selected_statistics( + ret = self.client.storage_node.query_selected_statistics( properties=["rfcacheFdReadTimeGreater5Sec"] ) - assert ret.get(self.fake_sds_id).get( + assert ret.get(self.fake_node_id).get( "rfcacheFdReadTimeGreater5Sec") == 0 - def test_sds_query_selected_statistics_bad_status(self): + def test_storage_node_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.sds.query_selected_statistics, + self.client.storage_node.query_selected_statistics, properties=["rfcacheFdReadTimeGreater5Sec"], ) From 3aa571b88414b0c9ad4964d09a1b511b77efae1d Mon Sep 17 00:00:00 2001 From: Yiming Bao Date: Thu, 24 Jul 2025 15:16:19 +0800 Subject: [PATCH 03/15] commit --- .gitignore | 4 + PyPowerFlex/__init__.py | 2 + PyPowerFlex/base_client.py | 13 + PyPowerFlex/objects/gen2/__init__.py | 4 + PyPowerFlex/objects/gen2/protection_domain.py | 531 +++++++++++++++ PyPowerFlex/objects/gen2/storage_pool.py | 642 ++++++++++++++++++ requirements.txt | 2 +- setup.py | 3 +- tests/requirements.txt | 1 - 9 files changed, 1199 insertions(+), 3 deletions(-) create mode 100644 PyPowerFlex/objects/gen2/protection_domain.py create mode 100644 PyPowerFlex/objects/gen2/storage_pool.py diff --git a/.gitignore b/.gitignore index f44edc6..04e571b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ __pycache__ # pytest coverage .coverage htmlcov + +build/ +dist/ +*.egg-info diff --git a/PyPowerFlex/__init__.py b/PyPowerFlex/__init__.py index d67d109..a5a9e8e 100644 --- a/PyPowerFlex/__init__.py +++ b/PyPowerFlex/__init__.py @@ -153,4 +153,6 @@ def add_objects_gen1(self): def add_objects_gen2(self): 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('utility', gen2.PowerFlexUtility) diff --git a/PyPowerFlex/base_client.py b/PyPowerFlex/base_client.py index 8bce0e2..171eb16 100644 --- a/PyPowerFlex/base_client.py +++ b/PyPowerFlex/base_client.py @@ -24,6 +24,7 @@ from PyPowerFlex import exceptions from PyPowerFlex import utils +from marshmallow import EXCLUDE, INCLUDE, Schema requests.packages.urllib3.disable_warnings(InsecureRequestWarning) LOG = logging.getLogger(__name__) @@ -607,3 +608,15 @@ def _query_selected_statistics(self, action, params=None): LOG.error(exc.message) raise exc return response + + +class BaseSchema(Schema): + def on_bind_field(self, field_name, field_obj): + field_obj.data_key = camelcase(field_obj.data_key or field_name) + + class Meta: + unknown = EXCLUDE + +def camelcase(s): + parts = iter(s.split("_")) + return next(parts) + "".join(i.title() for i in parts) diff --git a/PyPowerFlex/objects/gen2/__init__.py b/PyPowerFlex/objects/gen2/__init__.py index efa7737..2d97233 100644 --- a/PyPowerFlex/objects/gen2/__init__.py +++ b/PyPowerFlex/objects/gen2/__init__.py @@ -16,9 +16,13 @@ """This module contains the objects for interacting with the PowerFlex 5.0+ APIs.""" from PyPowerFlex.objects.gen2.storage_node import StorageNode +from PyPowerFlex.objects.gen2.protection_domain import ProtectionDomain +from PyPowerFlex.objects.gen2.storage_pool import StoragePool from PyPowerFlex.objects.gen2.utility import PowerFlexUtility __all__ = [ 'StorageNode', + 'ProtectionDomain', + 'StoragePool', 'PowerFlexUtility', ] diff --git a/PyPowerFlex/objects/gen2/protection_domain.py b/PyPowerFlex/objects/gen2/protection_domain.py new file mode 100644 index 0000000..b5a6e67 --- /dev/null +++ b/PyPowerFlex/objects/gen2/protection_domain.py @@ -0,0 +1,531 @@ +# 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 protection domain APIs.""" + +# pylint: disable=too-few-public-methods,no-member,too-many-arguments,too-many-positional-arguments,duplicate-code + +import logging +import requests + +from marshmallow import fields, validate +from PyPowerFlex import base_client, exceptions + + +LOG = logging.getLogger(__name__) + + +# class LinkSchema(Schema): +# rel = fields.Str( +# metadata={ +# "description": "Rel", +# } +# ) +# href = fields.Str( +# metadata={ +# "description": "Href", +# } +# ) +# def on_bind_field(self, field_name, field_obj): +# field_obj.data_key = camelcase(field_obj.data_key or field_name) +# class Meta: +# unknown = EXCLUDE + + +class ProtectionDomainSchema(base_client.BaseSchema): + id = fields.Str( + metadata={ + "description": "Protection Domain Id", + } + ) + name = fields.Str( + required=True, + metadata={ + "description": "Protection Domain Name", + "updatable": True, + } + ) + state = fields.Str( + validate=validate.OneOf(["Active", "Inactive"]), + data_key="protectionDomainState", + metadata={ + "description": "Protection Domain State: Active/Inactive, default: Active", + "updatable": True, + } + ) + rebuild_enabled = fields.Boolean( + metadata={ + "description": "Enable rebuild, default: True", + "updatable": True, + } + ) + rebalance_enabled = fields.Boolean( + metadata={ + "description": "Enable rebalance, default: True", + "updatable": True, + } + ) + rebuild_network_throttling_enabled = fields.Boolean( + metadata={ + "description": "Rebuild network throttling enabled", + } + ) + rebalance_network_throttling_enabled = fields.Boolean( + metadata={ + "description": "Rebalance network throttling enabled", + } + ) + gen_type = fields.Str( + metadata={ + "description": "Gen Type: EC or Mirroring", + } + ) + overall_concurrent_io_limit = fields.Integer( + metadata={ + "description": "Overall concurrent IO limit, default: 4", + "updatable": True, + } + ) + bandwidth_limit_overall_ios = fields.Integer( + metadata={ + "description": "Bandwidth limit overall IOs, default: 400", + "updatable": True, + } + ) + bandwidth_limit_bg_dev_scanner = fields.Integer( + metadata={ + "description": "Bandwidth limit background device scanner, default: 10", + "updatable": True, + } + ) + bandwidth_limit_garbage_collector = fields.Integer( + metadata={ + "description": "Bandwidth limit garbage collector, default: 65535", + "updatable": True, + } + ) + bandwidth_limit_singly_impacted_rebuild = fields.Integer( + metadata={ + "description": "Bandwidth limit singly impacted rebuild, default: 400", + "updatable": True, + } + ) + bandwidth_limit_doubly_impacted_rebuild = fields.Integer( + metadata={ + "description": "Bandwidth limit doubly impacted rebuild, default: 400", + "updatable": True, + } + ) + bandwidth_limit_rebalance = fields.Integer( + metadata={ + "description": "Bandwidth limit rebalance, default: 40", + "updatable": True, + } + ) + bandwidth_limit_other = fields.Integer( + metadata={ + "description": "Bandwidth limit rebalance, default: 10", + "updatable": True, + } + ) + bandwidth_limit_node_network = fields.Integer( + metadata={ + "description": "Bandwidth limit node network, default: 25", + "updatable": True, + } + ) + # links = fields.List(fields.Nested(LinkSchema), + # metadata={ + # "description": "Links", + # } + # ) + + +def load_protection_domain_schema(obj): + return ProtectionDomainSchema().load(obj) + + +class ProtectionDomain(base_client.EntityRequest): + """ + A class representing Protection Domain client. + """ + + def list(self): + """List PowerFlex protection domains. + + :rtype: list[dict] + """ + return list(map(load_protection_domain_schema, self.get())) + + def get_by_id(self, id): + """Get PowerFlex protection domain. + + :type id: str + :rtype: dict + """ + return load_protection_domain_schema(self.get(entity_id=id)) + + def get_by_name(self, name): + """Get PowerFlex protection domain. + + :type name: str + :rtype: dict + """ + result = self.get(filter_fields={'name': name}) + if len(result) >= 1: + return load_protection_domain_schema(result[0]) + else: + return None + + def delete(self, id): + """Remove PowerFlex protection domain. + + :type id: str + :rtype: None + """ + return self._delete_entity(id) + + def create(self, pd): + """Create PowerFlex protection domain. + + :type pd: dict + :rtype: dict + """ + pd = load_protection_domain_schema(pd) + params = {"name": pd['name']} + + new_pd = load_protection_domain_schema(self._create_entity(params)) + pd['id'] = new_pd['id'] + _, pd = self.update(ProtectionDomainSchema().dump(pd)) + + return pd + + def update(self, pd): + """Update PowerFlex protection domain. + + :type pd: dict + :rtype: dict + """ + current_pd = self.get_by_id(pd['id']) + pd = load_protection_domain_schema( + {**ProtectionDomainSchema().dump(current_pd), **pd}) + + has_update = False + + if pd['name'] != current_pd['name']: + has_update = True + self.rename(pd['id'], pd['name']) + + if pd['state'] != current_pd['state']: + has_update = True + if pd['state'] == "Inactive": + self.inactivate(pd['id'], force=True) + else: + self.activate(pd['id'], force=True) + + if pd['rebuild_enabled'] != current_pd['rebuild_enabled']: + has_update = True + self.set_rebuild_enabled(pd['id'], pd['rebuild_enabled']) + if pd['rebalance_enabled'] != current_pd['rebalance_enabled']: + has_update = True + self.set_rebalance_enabled(pd['id'], pd['rebalance_enabled']) + # self.disable_inflight_bandwidth_flow_control(pd['id']) + # self.enable_inflight_bandwidth_flow_control(pd['id']) + + policy = { + # TODO: unlimited, favorApplication + "policy": "favorApplication", + } + if pd['overall_concurrent_io_limit'] != current_pd['overall_concurrent_io_limit']: + policy['overallConcurrentIoLimit'] = pd['overall_concurrent_io_limit'] + if pd['bandwidth_limit_overall_ios'] != current_pd['bandwidth_limit_overall_ios']: + policy['bandwidthLimitOverallIos'] = pd['bandwidth_limit_overall_ios'] + if pd['bandwidth_limit_bg_dev_scanner'] != current_pd['bandwidth_limit_bg_dev_scanner']: + policy['bandwidthLimitBgDevScanner'] = pd['bandwidth_limit_bg_dev_scanner'] + if pd['bandwidth_limit_garbage_collector'] != current_pd['bandwidth_limit_garbage_collector']: + policy['bandwidthLimitGarbageCollector'] = pd['bandwidth_limit_garbage_collector'] + if pd['bandwidth_limit_singly_impacted_rebuild'] != current_pd['bandwidth_limit_singly_impacted_rebuild']: + policy['bandwidthLimitSinglyImpactedRebuild'] = pd['bandwidth_limit_singly_impacted_rebuild'] + if pd['bandwidth_limit_doubly_impacted_rebuild'] != current_pd['bandwidth_limit_doubly_impacted_rebuild']: + policy['bandwidthLimitDoublyImpactedRebuild'] = pd['bandwidth_limit_doubly_impacted_rebuild'] + if pd['bandwidth_limit_rebalance'] != current_pd['bandwidth_limit_rebalance']: + policy['bandwidthLimitRebalance'] = pd['bandwidth_limit_rebalance'] + if pd['bandwidth_limit_other'] != current_pd['bandwidth_limit_other']: + policy['bandwidthLimitOther'] = pd['bandwidth_limit_other'] + if pd['bandwidth_limit_node_network'] != current_pd['bandwidth_limit_node_network']: + policy['bandwidthLimitNodeNetwork'] = pd['bandwidth_limit_node_network'] + + if len(policy) > 1: + has_update = True + self.set_secondary_io_policy(pd['id'], policy) + + return has_update, self.get_by_id(pd['id']) + + def activate(self, protection_domain_id, force=False): + """Activate PowerFlex protection domain. + + :type protection_domain_id: str + :type force: bool + :rtype: None + """ + + action = 'activateProtectionDomain' + + params = { + "forceActivate": force + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=protection_domain_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to activate PowerFlex {self.entity} " + f"with id {protection_domain_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def inactivate(self, protection_domain_id, force=False): + """Inactivate PowerFlex protection domain. + + :type protection_domain_id: str + :type force: bool + :rtype: None + """ + + action = 'inactivateProtectionDomain' + + params = { + "forceShutdown": force + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=protection_domain_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to inactivate PowerFlex {self.entity} " + f"with id {protection_domain_id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def enable_inflight_bandwidth_flow_control(self, id): + """Enable inflight bandwidth flow control. + + :type id: str + :rtype: None + """ + + action = 'enableInflightBandwidthFlowControl' + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=id) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to enable inflight bandwidth flow control in PowerFlex {self.entity} " + f"with id {id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def disable_inflight_bandwidth_flow_control(self, id): + """Disable inflight bandwidth flow control. + + :type id: str + :rtype: None + """ + + action = 'disableInflightBandwidthFlowControl' + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=id) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to disable inflight bandwidth flow control in PowerFlex {self.entity} " + f"with id {id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def set_rebuild_enabled(self, id, enabled): + """Set rebuild state. + + :type id: str + :type enabled: bool + :rtype: None + """ + + action = 'setRebuildEnabled' + params = { + "rebuildEnabled": enabled, + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to set rebuild state in PowerFlex {self.entity} " + f"with id {id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def set_rebalance_enabled(self, id, enabled): + """Set rebalance state. + + :type id: str + :type enabled: bool + :rtype: None + """ + + action = 'setRebalanceEnabled' + params = { + "rebalanceEnabled": enabled, + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to set rebalance state in PowerFlex {self.entity} " + f"with id {id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def set_secondary_io_policy(self, id, policy): + """Set secondary I/O policy. + + :type id: str + :type policy: Dict + :rtype: None + """ + + action = 'setSecondaryIoPolicy' + params = { + "policy": policy["policy"], + } + if 'overallConcurrentIoLimit' in policy: + params["overallConcurrentIoLimit"] = policy["overallConcurrentIoLimit"] + if 'bandwidwith_limit_overall_ios' in policy: + params['bandwidthLimitOverallIos'] = policy['bandwidwith_limit_overall_ios'] + if 'bandwidth_limit_bg_dev_scanner' in policy: + params['bandwidthLimitBgDevScanner'] = policy['bandwidth_limit_bg_dev_scanner'] + if 'bandwidth_limit_garbage_collector' in policy: + params['bandwidthLimitGarbageCollector'] = policy['bandwidth_limit_garbage_collector'] + if 'bandwidth_limit_singly_impacted_rebuild' in policy: + params['bandwidthLimitSinglyImpactedRebuild'] = policy['bandwidth_limit_singly_impacted_rebuild'] + if 'bandwidth_limit_doubly_impacted_rebuild' in policy: + params['bandwidthLimitDoublyImpactedRebuild'] = policy['bandwidth_limit_doubly_impacted_rebuild'] + if 'bandwidth_limit_rebalance' in policy: + params['bandwidthLimitRebalance'] = policy['bandwidth_limit_rebalance'] + if 'bandwidth_limit_other' in policy: + params['bandwidthLimitOther'] = policy['bandwidth_limit_other'] + if 'bandwidth_limit_node_network' in policy: + params['bandwidthLimitNodeNetwork'] = policy['bandwidth_limit_node_network'] + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to set secondary I/O policy in PowerFlex {self.entity} " + f"with id {id}. Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def get_sdss(self, protection_domain_id, filter_fields=None, fields=None): + """Get related PowerFlex SDSs for protection domain. + + :type protection_domain_id: str + :type filter_fields: dict + :type fields: list|tuple + :rtype: list[dict] + """ + + return self.get_related(protection_domain_id, + 'Sds', + filter_fields, + fields) + + def get_storage_pools(self, + protection_domain_id, + filter_fields=None, + fields=None): + """Get related PowerFlex storage pools for protection domain. + + :type protection_domain_id: str + :type filter_fields: dict + :type fields: list|tuple + :rtype: list[dict] + """ + + return self.get_related(protection_domain_id, + 'StoragePool', + filter_fields, + fields) + + def rename(self, protection_domain_id, name): + """Rename PowerFlex protection domain. + + :type protection_domain_id: str + :type name: str + :rtype: None + """ + + action = 'setProtectionDomainName' + + params = {"name": name} + + self._rename_entity(action, protection_domain_id, params) + + def query_selected_statistics(self, properties, ids=None): + """Query PowerFlex protection domain statistics. + + :type properties: list + :type ids: list of protection domain IDs or None for all protection + domains + :rtype: dict + """ + + action = "querySelectedStatistics" + + params = {'properties': properties} + + if ids: + params["ids"] = ids + else: + params["allIds"] = "" + + return self._query_selected_statistics(action, params) diff --git a/PyPowerFlex/objects/gen2/storage_pool.py b/PyPowerFlex/objects/gen2/storage_pool.py new file mode 100644 index 0000000..0cecf38 --- /dev/null +++ b/PyPowerFlex/objects/gen2/storage_pool.py @@ -0,0 +1,642 @@ +# 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 storage pool APIs.""" + +# pylint: disable=too-few-public-methods,too-many-public-methods,no-member,too-many-arguments,too-many-positional-arguments,too-many-locals,cyclic-import,duplicate-code + +import logging +import requests + +from marshmallow import INCLUDE, fields, validate, validates_schema, ValidationError +from PyPowerFlex import base_client, exceptions +from PyPowerFlex.objects.gen2.protection_domain import ProtectionDomain + + +LOG = logging.getLogger(__name__) + + +def validate_over_provisioning_factor(value): + if value != 0 and (value < 100 or value > 10000): + raise ValidationError("Not an valid value.") + + +class StoragePoolSchema(base_client.BaseSchema): + id = fields.Str( + metadata={ + "description": "Storage Pool Id", + } + ) + name = fields.Str( + allow_none=True, + metadata={ + "description": "Storage Pool Name", + "updatable": True, + } + ) + protection_domain_id = fields.Str( + required=True, + metadata={ + "description": "Protection Domain Id", + "updatable": False, + } + ) + device_group_id = fields.Str( + required=True, + metadata={ + "description": "Device Group Id", + "updatable": False, + } + ) + wrc_device_group_id = fields.Str( + metadata={ + "description": "Device Group Id", + # "updatable": False, + } + ) + gen_type = fields.Str( + # required=True, # 5.0.0 only supports EC type + metadata={ + "description": "Gen Type, EC or MIRRORING", + } + ) + capacity_alert_high_threshold = fields.Integer( + metadata={ + "description": "Capacity Alert High Threshold, default: 80", + "updatable": True, + } + ) + capacity_alert_critical_threshold = fields.Integer( + metadata={ + "description": "Capacity Alert Critical Threshold, default: 90", + "updatable": True, + } + ) + fragmentation_enabled = fields.Boolean( + metadata={ + "description": "Enable Fragmentation, default: False", + # "updatable": True, + } + ) + over_provisioning_factor = fields.Integer( + validate=validate.And(validate_over_provisioning_factor), + metadata={ + "description": "Over Provisioning Factor, range: 0, 100-10000, set 0 to disable over provisioning. Default: 0", + } + ) + physical_size_gb = fields.Integer( + required=True, + data_key="physicalSizeGB", + metadata={ + "description": "Physical Size in GB, set -1 to use all available capacity. It only accepts larger value during update.", + "updatable": True, + } + ) + raw_size_gb = fields.Integer( + data_key="rawSizeGB", + metadata={ + "description": "Raw Size in GB", + } + ) + # num_data_slices = fields.Integer( + # required=True, + # metadata={ + # "description": "Number of Data Slices", + # } + # ) + # numProtectionSlices = fields.Integer( + # required=True, + # metadata={ + # "description": "Number of Protection Slices", + # } + # ) + protection_scheme = fields.Str( + required=True, + validate=validate.OneOf(["TwoPlusTwo", "EightPlusTwo"]), + metadata={ + "description": "Protection Scheme: TwoPlusTwo/EightPlusTwo", + "updatable": False, + } + ) + compression_method = fields.Str( + validate=validate.OneOf(["None", "Normal"]), + metadata={ + "description": "Compression Method: None/Normal. Default: Normal", + "updatable": True, + } + ) + zero_padding_enabled = fields.Boolean( + metadata={ + "description": "Zero padding enabled. Default: True", + } + ) + + # @validates_schema + # def validate_capacity_alert_threshold(self, data, **kwargs): + # if data["capacity_alert_high_threshold"] >= data["capacity_alert_critical_threshold"]: + # raise ValidationError("capacity_alert_critical_threshold must be greater than capacity_alert_high_threshold") + + # class Meta: + # unknown = INCLUDE + + +def load_storage_pool_schema(obj): + return StoragePoolSchema().load(obj) + + +class StoragePool(base_client.EntityRequest): + """ + A class representing Storage Pool client. + """ + + def list(self): + """List PowerFlex storage pools. + + :rtype: list[dict] + """ + return list(map(load_storage_pool_schema, self.get())) + + def get_by_id(self, id): + """Get PowerFlex storage pool. + + :type id: str + :rtype: dict + """ + return load_storage_pool_schema(self.get(entity_id=id)) + + def get_by_name(self, protion_domain_id, name): + """Get PowerFlex storage pool. + + :type protection_domain_id: str + :type name: str + :rtype: dict + """ + pdo = ProtectionDomain(self.token, self.configuration) + + result = pdo.get_storage_pools( + protion_domain_id, filter_fields={'name': name}) + if len(result) >= 1: + return load_storage_pool_schema(result[0]) + else: + return None + + def create(self, sp): + """Create PowerFlex storage pool. + + :type sp: dict + :rtype: dict + """ + sp = load_storage_pool_schema(sp) + + params = { + "protectionDomainId": sp['protection_domain_id'], + "deviceGroupId": sp['device_group_id'], + "gen": "EC", + } + + if "name" in sp: + params["name"] = sp["name"] + if "compression_method" in sp: + params["compressionMethod"] = sp["compression_method"] + + if sp["protection_scheme"] == "TwoPlusTwo": + params["numDataSlices"] = 2 + params["numProtectionSlices"] = 2 + else: + params["numDataSlices"] = 8 + params["numProtectionSlices"] = 2 + + if sp["physical_size_gb"] == -1: + params["useAllAvailableCapacity"] = True + else: + params["physicalSizeGB"] = sp["physical_size_gb"] + + new_sp = load_storage_pool_schema(self._create_entity(params)) + sp['id'] = new_sp['id'] + _, sp = self.update(StoragePoolSchema().dump(sp)) + + return sp + + def update(self, sp): + """Update PowerFlex storage pool. + + :type sp: dict + :rtype: dict + """ + current_sp = self.get_by_id(sp['id']) + sp = load_storage_pool_schema( + {**StoragePoolSchema().dump(current_sp), **sp}) + + if sp['protection_domain_id'] != current_sp['protection_domain_id']: + e = exceptions.nonupdatable_exception( + "protection_domain_id", self.entity, sp['id']) + LOG.error(e.message) + raise e + if sp['device_group_id'] != current_sp['device_group_id']: + e = exceptions.nonupdatable_exception( + "device_group_id", self.entity, sp['id']) + LOG.error(e.message) + raise e + if sp['protection_scheme'] != current_sp['protection_scheme']: + e = exceptions.nonupdatable_exception( + "protection_scheme", self.entity, sp['id']) + LOG.error(e.message) + raise e + + has_update = False + + if sp['name'] != current_sp['name']: + has_update = True + self.rename(sp['id'], sp['name']) + + high_threshold = None + critical_threshold = None + if sp['capacity_alert_high_threshold'] != current_sp['capacity_alert_high_threshold']: + high_threshold = sp['capacity_alert_high_threshold'] + if sp['capacity_alert_critical_threshold'] != current_sp['capacity_alert_critical_threshold']: + critical_threshold = sp['capacity_alert_critical_threshold'] + if high_threshold or critical_threshold: + has_update = True + self.set_capacity_alert_thresholds( + sp['id'], high_threshold, critical_threshold) + + # TODO: add support for fragmentation, currently update would fail with error 'Could not find Storage Pool' + # if sp['fragmentation_enabled'] != current_sp['fragmentation_enabled']: + # has_update = True + # self.set_fragmentation_enabled(sp['id'], sp['fragmentation_enabled']) + + if sp['over_provisioning_factor'] != current_sp['over_provisioning_factor']: + has_update = True + self.set_over_provisioning_factor( + sp['id'], sp['over_provisioning_factor']) + + if sp['physical_size_gb'] != current_sp['physical_size_gb']: + has_update = True + self.resize(sp['id'], sp['physical_size_gb']) + + if sp['compression_method'] != current_sp['compression_method']: + has_update = True + self.set_compression_method(sp['id'], sp['compression_method']) + + return has_update, self.get_by_id(sp['id']) + + def delete(self, storage_pool_id): + """Remove PowerFlex storage pool. + + :type storage_pool_id: str + :rtype: None + """ + + return self._delete_entity(storage_pool_id) + + def get_devices(self, storage_pool_id, filter_fields=None, fields=None): + """Get related PowerFlex devices for storage pool. + + :type storage_pool_id: str + :type filter_fields: dict + :type fields: list|tuple + :rtype: list[dict] + """ + + return self.get_related(storage_pool_id, + 'Device', + filter_fields, + fields) + + def get_sdss(self, storage_pool_id, filter_fields=None, fields=None): + """Get related PowerFlex SDSs for storage pool. + + :type storage_pool_id: str + :type filter_fields: dict + :type fields: list|tuple + :rtype: list[dict] + """ + + sdss_ids = self.get_related(storage_pool_id, + 'SpSds', + filter_fields, + fields=('sdsId',)) + sds_id_list = [sds['sdsId'] for sds in sdss_ids] + if filter_fields: + filter_fields.update({'id': sds_id_list}) + filter_fields.pop('sdsId', None) + else: + filter_fields = {'id': sds_id_list} + return Sds(self.token, self.configuration).get( + filter_fields=filter_fields, fields=fields) + + def get_volumes(self, storage_pool_id, filter_fields=None, fields=None): + """Get related PowerFlex volumes for storage pool. + + :type storage_pool_id: str + :type filter_fields: dict + :type fields: list|tuple + :rtype: list[dict] + """ + + return self.get_related(storage_pool_id, + 'Volume', + filter_fields, + fields) + + def get_statistics(self, storage_pool_id, fields=None): + """Get related PowerFlex Statistics for storage pool. + + :type storage_pool_id: str + :type fields: list|tuple + :rtype: dict + """ + + return self.get_related(storage_pool_id, + 'Statistics', + fields) + + def rename(self, storage_pool_id, name): + """Rename PowerFlex storage pool. + + :type storage_pool_id: str + :type name: str + :rtype: None + """ + + action = 'renameStoragePool' + + params = {"newName": name} + self._rename_entity(action, storage_pool_id, params) + + def set_capacity_alert_thresholds(self, storage_pool_id, high_threshold, critical_threshold): + """Set the capacity alert thresholds for the specified Storage Pool. + + :type high_threshold: int + :type critical_threshold: int + :rtype: None + """ + + action = 'setCapacityAlertThresholds' + + params = { + "capacityAlertHighThresholdPercent": high_threshold, + "capacityAlertCriticalThresholdPercent": critical_threshold + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=storage_pool_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f'Failed to set the capacity alert thresholds for PowerFlex {self.entity}' + f'with id {storage_pool_id}. Error: {response}') + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def set_over_provisioning_factor(self, storage_pool_id, over_provisioning_factor): + """Set the over provisioning factor for PowerFlex storage pool. + + :type storage_pool_id: str + :type over_provisioning_factor: int + :rtype: None + """ + + action = 'setOverProvisioningFactor' + + params = { + "overProvisioningFactor": over_provisioning_factor + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=storage_pool_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f'Failed to set the over provisioning factor for PowerFlex {self.entity}' + f' with id {storage_pool_id}. Error: {response}') + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def resize(self, storage_pool_id, size_in_gb): + """Set the size for PowerFlex storage pool. + + :type storage_pool_id: str + :type size: int + :rtype: None + """ + + action = 'modifyStoragePoolSize' + + params = { + "physicalSizeGB": size_in_gb + } + + if size_in_gb == -1: + params = { + "useAllAvailableCapacity": True + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=storage_pool_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f'Failed to modify the size for PowerFlex {self.entity}' + f' with id {storage_pool_id}. Error: {response}') + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def set_compression_method(self, storage_pool_id, compression_method): + """Set compression method for PowerFlex storage pool. + + :type storage_pool_id: str + :type compression_method: str + :rtype: dict + """ + + action = 'modifyCompressionMethod' + + params = { + "compressionMethod": compression_method + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=storage_pool_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f'Failed to set compression method for PowerFlex {self.entity} ' + f'with id {storage_pool_id}. Error: {response}') + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def set_zero_padding_policy(self, storage_pool_id, zero_padding_enabled): + """Enable/disable zero padding for PowerFlex storage pool. + + :type storage_pool_id: str + :type zero_padding_enabled: bool + :rtype: None + """ + + action = 'setZeroPaddingPolicy' + + params = {"zeroPadEnabled": zero_padding_enabled} + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=storage_pool_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f'Failed to set Zero Padding policy for PowerFlex {self.entity} ' + f'with id {storage_pool_id}. Error: {response}') + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + def set_protected_maintenance_mode_io_priority_policy( + self, storage_pool_id, policy, concurrent_ios_per_device, bw_limit_per_device): + """Set protected maintenance mode I/O priority policy. + + :type storage_pool_id: str + :type policy: str + :type concurrent_ios_per_device: str + :type bw_limit_per_device: str + :rtype: dict + """ + + action = 'setProtectedMaintenanceModeIoPriorityPolicy' + + params = { + 'policy': policy, + 'numOfConcurrentIosPerDevice': concurrent_ios_per_device, + 'bwLimitPerDeviceInKbps': bw_limit_per_device + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=storage_pool_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f'Failed to set the protected maintenance mode IO priority policy for ' + f'PowerFlex {self.entity} with id {storage_pool_id}. Error: {response}' + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=storage_pool_id) + + def set_vtree_migration_io_priority_policy( + self, + storage_pool_id, + policy, + concurrent_ios_per_device, + bw_limit_per_device): + """Set the vtree migration I/O priority policy on the specified Storage Pool. + + :type storage_pool_id: str + :type policy: str + :type concurrent_ios_per_device: str + :type bw_limit_per_device: str + :rtype: dict + """ + + action = 'setVTreeMigrationIoPriorityPolicy' + + params = { + "policy": policy, + "numOfConcurrentIosPerDevice": concurrent_ios_per_device, + "bwLimitPerDeviceInKbps": bw_limit_per_device + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=storage_pool_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f'Failed to set VTree migration I/O priority policy for PowerFlex {self.entity} ' + f'with id {storage_pool_id}. Error: {response}') + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=storage_pool_id) + + def rebalance_io_priority_policy( + self, + storage_pool_id, + policy, + concurrent_ios_per_device, + bw_limit_per_device): + """Set the rebalance I/O priority policy on the specified Storage Pool. + + :type storage_pool_id: str + :type policy: str + :type concurrent_ios_per_device: str + :type bw_limit_per_device: str + :rtype: dict + """ + + action = 'setRebalanceIoPriorityPolicy' + + params = { + 'policy': policy, + 'numOfConcurrentIosPerDevice': concurrent_ios_per_device, + 'bwLimitPerDeviceInKbps': bw_limit_per_device + } + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=storage_pool_id, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f'Failed to set the rebalance I/O priority policy for PowerFlex {self.entity} ' + f'with id {storage_pool_id}. Error: {response}') + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return self.get(entity_id=storage_pool_id) + + def set_fragmentation_enabled(self, storage_pool_id, enable_fragmentation): + """Enable/Disable the fragmentation on the specified Storage Pool. + + :type storage_pool_id: str + :type enable_fragmentation: bool + :rtype: dict + """ + + action = 'disableFragmentation' + if enable_fragmentation: + action = 'enableFragmentation' + + r, response = self.send_post_request(self.base_action_url, + action=action, + entity=self.entity, + entity_id=storage_pool_id) + if r.status_code != requests.codes.ok: + msg = ( + f'Failed to enable/disable fragmentation for PowerFlex {self.entity} ' + f'with id {storage_pool_id}. Error: {response}') + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) diff --git a/requirements.txt b/requirements.txt index 7651129..383b1ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ requests packaging - +marshmallow==4.0.0 diff --git a/setup.py b/setup.py index 065bca6..38cc843 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ setup( name='PyPowerFlex', - version='1.15.0', + version='1.15.1', description='Python library for Dell PowerFlex', author='Ansible Team at Dell', author_email='ansible.team@dell.com', @@ -33,6 +33,7 @@ classifiers=['License :: OSI Approved :: Apache Software License'], packages=[ 'PyPowerFlex', + 'PyPowerFlex.objects.common', 'PyPowerFlex.objects.gen1', 'PyPowerFlex.objects.gen2', ], diff --git a/tests/requirements.txt b/tests/requirements.txt index 9b342fd..5423c8d 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,4 +1,3 @@ testtools pytest pytest-coverage - From 22b0e767df0f48b9923c95f423e08ae0296a0dc8 Mon Sep 17 00:00:00 2001 From: Luis Liu Date: Mon, 28 Jul 2025 10:26:45 +0800 Subject: [PATCH 04/15] Update storage pool and protection domain Signed-off-by: Luis Liu --- .gitignore | 2 + PyPowerFlex/exceptions.py | 7 + PyPowerFlex/objects/gen2/protection_domain.py | 62 ++-- PyPowerFlex/objects/gen2/storage_pool.py | 305 +++++------------- setup.py | 2 +- 5 files changed, 124 insertions(+), 254 deletions(-) diff --git a/.gitignore b/.gitignore index 04e571b..0ae1d88 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ htmlcov build/ dist/ *.egg-info + +test.py diff --git a/PyPowerFlex/exceptions.py b/PyPowerFlex/exceptions.py index 2526e09..e7c6d9a 100644 --- a/PyPowerFlex/exceptions.py +++ b/PyPowerFlex/exceptions.py @@ -130,3 +130,10 @@ def __init__(self, entity, entity_id, action, response=None): self.response = response if response: self.message = f"{self.message} Error: {response}" + + +def nonupdatable_exception(field, entity, entity_id=None): + msg = ( + f'{field} cannot be updated after creation for PowerFlex {entity} {entity_id}' + ) + return PowerFlexClientException(msg) diff --git a/PyPowerFlex/objects/gen2/protection_domain.py b/PyPowerFlex/objects/gen2/protection_domain.py index b5a6e67..47e72f7 100644 --- a/PyPowerFlex/objects/gen2/protection_domain.py +++ b/PyPowerFlex/objects/gen2/protection_domain.py @@ -161,7 +161,6 @@ class ProtectionDomain(base_client.EntityRequest): """ A class representing Protection Domain client. """ - def list(self): """List PowerFlex protection domains. @@ -208,19 +207,17 @@ def create(self, pd): new_pd = load_protection_domain_schema(self._create_entity(params)) pd['id'] = new_pd['id'] - _, pd = self.update(ProtectionDomainSchema().dump(pd)) - + _, pd = self.update(ProtectionDomainSchema().dump(pd), new_pd) return pd - def update(self, pd): + def update(self, pd, current_pd=None): """Update PowerFlex protection domain. :type pd: dict :rtype: dict """ - current_pd = self.get_by_id(pd['id']) - pd = load_protection_domain_schema( - {**ProtectionDomainSchema().dump(current_pd), **pd}) + current_pd = current_pd if current_pd is not None else self.get_by_id(pd['id']) + pd = load_protection_domain_schema({**ProtectionDomainSchema().dump(current_pd), **pd}) has_update = False @@ -266,7 +263,6 @@ def update(self, pd): policy['bandwidthLimitOther'] = pd['bandwidth_limit_other'] if pd['bandwidth_limit_node_network'] != current_pd['bandwidth_limit_node_network']: policy['bandwidthLimitNodeNetwork'] = pd['bandwidth_limit_node_network'] - if len(policy) > 1: has_update = True self.set_secondary_io_policy(pd['id'], policy) @@ -465,19 +461,19 @@ def set_secondary_io_policy(self, id, policy): LOG.error(msg) raise exceptions.PowerFlexClientException(msg) - def get_sdss(self, protection_domain_id, filter_fields=None, fields=None): - """Get related PowerFlex SDSs for protection domain. + # def get_storage_nodes(self, protection_domain_id, filter_fields=None, fields=None): + # """Get related PowerFlex Storage Nodes for protection domain. - :type protection_domain_id: str - :type filter_fields: dict - :type fields: list|tuple - :rtype: list[dict] - """ + # :type protection_domain_id: str + # :type filter_fields: dict + # :type fields: list|tuple + # :rtype: list[dict] + # """ - return self.get_related(protection_domain_id, - 'Sds', - filter_fields, - fields) + # return self.get_related(protection_domain_id, + # 'StorageNode', + # filter_fields, + # fields) def get_storage_pools(self, protection_domain_id, @@ -510,22 +506,22 @@ def rename(self, protection_domain_id, name): self._rename_entity(action, protection_domain_id, params) - def query_selected_statistics(self, properties, ids=None): - """Query PowerFlex protection domain statistics. + # def query_selected_statistics(self, properties, ids=None): + # """Query PowerFlex protection domain statistics. - :type properties: list - :type ids: list of protection domain IDs or None for all protection - domains - :rtype: dict - """ + # :type properties: list + # :type ids: list of protection domain IDs or None for all protection + # domains + # :rtype: dict + # """ - action = "querySelectedStatistics" + # action = "querySelectedStatistics" - params = {'properties': properties} + # params = {'properties': properties} - if ids: - params["ids"] = ids - else: - params["allIds"] = "" + # if ids: + # params["ids"] = ids + # else: + # params["allIds"] = "" - return self._query_selected_statistics(action, params) + # return self._query_selected_statistics(action, params) diff --git a/PyPowerFlex/objects/gen2/storage_pool.py b/PyPowerFlex/objects/gen2/storage_pool.py index 0cecf38..4d78ec3 100644 --- a/PyPowerFlex/objects/gen2/storage_pool.py +++ b/PyPowerFlex/objects/gen2/storage_pool.py @@ -63,6 +63,7 @@ class StoragePoolSchema(base_client.BaseSchema): wrc_device_group_id = fields.Str( metadata={ "description": "Device Group Id", + # TODO: # "updatable": False, } ) @@ -110,18 +111,6 @@ class StoragePoolSchema(base_client.BaseSchema): "description": "Raw Size in GB", } ) - # num_data_slices = fields.Integer( - # required=True, - # metadata={ - # "description": "Number of Data Slices", - # } - # ) - # numProtectionSlices = fields.Integer( - # required=True, - # metadata={ - # "description": "Number of Protection Slices", - # } - # ) protection_scheme = fields.Str( required=True, validate=validate.OneOf(["TwoPlusTwo", "EightPlusTwo"]), @@ -185,8 +174,7 @@ def get_by_name(self, protion_domain_id, name): """ pdo = ProtectionDomain(self.token, self.configuration) - result = pdo.get_storage_pools( - protion_domain_id, filter_fields={'name': name}) + result = pdo.get_storage_pools(protion_domain_id, filter_fields={'name': name}) if len(result) >= 1: return load_storage_pool_schema(result[0]) else: @@ -225,33 +213,29 @@ def create(self, sp): new_sp = load_storage_pool_schema(self._create_entity(params)) sp['id'] = new_sp['id'] - _, sp = self.update(StoragePoolSchema().dump(sp)) + _, sp = self.update(StoragePoolSchema().dump(sp), new_sp) return sp - def update(self, sp): + def update(self, sp, current_sp=None): """Update PowerFlex storage pool. :type sp: dict :rtype: dict """ - current_sp = self.get_by_id(sp['id']) - sp = load_storage_pool_schema( - {**StoragePoolSchema().dump(current_sp), **sp}) + current_sp = current_sp if current_sp is not None else self.get_by_id(sp['id']) + sp = load_storage_pool_schema({**StoragePoolSchema().dump(current_sp), **sp}) if sp['protection_domain_id'] != current_sp['protection_domain_id']: - e = exceptions.nonupdatable_exception( - "protection_domain_id", self.entity, sp['id']) + e = exceptions.nonupdatable_exception("protection_domain_id", self.entity, sp['id']) LOG.error(e.message) raise e if sp['device_group_id'] != current_sp['device_group_id']: - e = exceptions.nonupdatable_exception( - "device_group_id", self.entity, sp['id']) + e = exceptions.nonupdatable_exception("device_group_id", self.entity, sp['id']) LOG.error(e.message) raise e if sp['protection_scheme'] != current_sp['protection_scheme']: - e = exceptions.nonupdatable_exception( - "protection_scheme", self.entity, sp['id']) + e = exceptions.nonupdatable_exception("protection_scheme", self.entity, sp['id']) LOG.error(e.message) raise e @@ -269,18 +253,11 @@ def update(self, sp): critical_threshold = sp['capacity_alert_critical_threshold'] if high_threshold or critical_threshold: has_update = True - self.set_capacity_alert_thresholds( - sp['id'], high_threshold, critical_threshold) - - # TODO: add support for fragmentation, currently update would fail with error 'Could not find Storage Pool' - # if sp['fragmentation_enabled'] != current_sp['fragmentation_enabled']: - # has_update = True - # self.set_fragmentation_enabled(sp['id'], sp['fragmentation_enabled']) + self.set_capacity_alert_thresholds(sp['id'], high_threshold, critical_threshold) if sp['over_provisioning_factor'] != current_sp['over_provisioning_factor']: has_update = True - self.set_over_provisioning_factor( - sp['id'], sp['over_provisioning_factor']) + self.set_over_provisioning_factor(sp['id'], sp['over_provisioning_factor']) if sp['physical_size_gb'] != current_sp['physical_size_gb']: has_update = True @@ -301,67 +278,67 @@ def delete(self, storage_pool_id): return self._delete_entity(storage_pool_id) - def get_devices(self, storage_pool_id, filter_fields=None, fields=None): - """Get related PowerFlex devices for storage pool. - - :type storage_pool_id: str - :type filter_fields: dict - :type fields: list|tuple - :rtype: list[dict] - """ - - return self.get_related(storage_pool_id, - 'Device', - filter_fields, - fields) - - def get_sdss(self, storage_pool_id, filter_fields=None, fields=None): - """Get related PowerFlex SDSs for storage pool. - - :type storage_pool_id: str - :type filter_fields: dict - :type fields: list|tuple - :rtype: list[dict] - """ - - sdss_ids = self.get_related(storage_pool_id, - 'SpSds', - filter_fields, - fields=('sdsId',)) - sds_id_list = [sds['sdsId'] for sds in sdss_ids] - if filter_fields: - filter_fields.update({'id': sds_id_list}) - filter_fields.pop('sdsId', None) - else: - filter_fields = {'id': sds_id_list} - return Sds(self.token, self.configuration).get( - filter_fields=filter_fields, fields=fields) - - def get_volumes(self, storage_pool_id, filter_fields=None, fields=None): - """Get related PowerFlex volumes for storage pool. - - :type storage_pool_id: str - :type filter_fields: dict - :type fields: list|tuple - :rtype: list[dict] - """ - - return self.get_related(storage_pool_id, - 'Volume', - filter_fields, - fields) - - def get_statistics(self, storage_pool_id, fields=None): - """Get related PowerFlex Statistics for storage pool. - - :type storage_pool_id: str - :type fields: list|tuple - :rtype: dict - """ - - return self.get_related(storage_pool_id, - 'Statistics', - fields) + # def get_devices(self, storage_pool_id, filter_fields=None, fields=None): + # """Get related PowerFlex devices for storage pool. + + # :type storage_pool_id: str + # :type filter_fields: dict + # :type fields: list|tuple + # :rtype: list[dict] + # """ + + # return self.get_related(storage_pool_id, + # 'Device', + # filter_fields, + # fields) + + # def get_sdss(self, storage_pool_id, filter_fields=None, fields=None): + # """Get related PowerFlex SDSs for storage pool. + + # :type storage_pool_id: str + # :type filter_fields: dict + # :type fields: list|tuple + # :rtype: list[dict] + # """ + + # sdss_ids = self.get_related(storage_pool_id, + # 'SpSds', + # filter_fields, + # fields=('sdsId',)) + # sds_id_list = [sds['sdsId'] for sds in sdss_ids] + # if filter_fields: + # filter_fields.update({'id': sds_id_list}) + # filter_fields.pop('sdsId', None) + # else: + # filter_fields = {'id': sds_id_list} + # return Sds(self.token, self.configuration).get( + # filter_fields=filter_fields, fields=fields) + + # def get_volumes(self, storage_pool_id, filter_fields=None, fields=None): + # """Get related PowerFlex volumes for storage pool. + + # :type storage_pool_id: str + # :type filter_fields: dict + # :type fields: list|tuple + # :rtype: list[dict] + # """ + + # return self.get_related(storage_pool_id, + # 'Volume', + # filter_fields, + # fields) + + # def get_statistics(self, storage_pool_id, fields=None): + # """Get related PowerFlex Statistics for storage pool. + + # :type storage_pool_id: str + # :type fields: list|tuple + # :rtype: dict + # """ + + # return self.get_related(storage_pool_id, + # 'Statistics', + # fields) def rename(self, storage_pool_id, name): """Rename PowerFlex storage pool. @@ -510,133 +487,21 @@ def set_zero_padding_policy(self, storage_pool_id, zero_padding_enabled): LOG.error(msg) raise exceptions.PowerFlexClientException(msg) - def set_protected_maintenance_mode_io_priority_policy( - self, storage_pool_id, policy, concurrent_ios_per_device, bw_limit_per_device): - """Set protected maintenance mode I/O priority policy. - - :type storage_pool_id: str - :type policy: str - :type concurrent_ios_per_device: str - :type bw_limit_per_device: str - :rtype: dict - """ - - action = 'setProtectedMaintenanceModeIoPriorityPolicy' - - params = { - 'policy': policy, - 'numOfConcurrentIosPerDevice': concurrent_ios_per_device, - 'bwLimitPerDeviceInKbps': bw_limit_per_device - } - - r, response = self.send_post_request(self.base_action_url, - action=action, - entity=self.entity, - entity_id=storage_pool_id, - params=params) - if r.status_code != requests.codes.ok: - msg = ( - f'Failed to set the protected maintenance mode IO priority policy for ' - f'PowerFlex {self.entity} with id {storage_pool_id}. Error: {response}' - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return self.get(entity_id=storage_pool_id) - - def set_vtree_migration_io_priority_policy( - self, - storage_pool_id, - policy, - concurrent_ios_per_device, - bw_limit_per_device): - """Set the vtree migration I/O priority policy on the specified Storage Pool. - - :type storage_pool_id: str - :type policy: str - :type concurrent_ios_per_device: str - :type bw_limit_per_device: str - :rtype: dict - """ - - action = 'setVTreeMigrationIoPriorityPolicy' - - params = { - "policy": policy, - "numOfConcurrentIosPerDevice": concurrent_ios_per_device, - "bwLimitPerDeviceInKbps": bw_limit_per_device - } - - r, response = self.send_post_request(self.base_action_url, - action=action, - entity=self.entity, - entity_id=storage_pool_id, - params=params) - if r.status_code != requests.codes.ok: - msg = ( - f'Failed to set VTree migration I/O priority policy for PowerFlex {self.entity} ' - f'with id {storage_pool_id}. Error: {response}') - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return self.get(entity_id=storage_pool_id) - - def rebalance_io_priority_policy( - self, - storage_pool_id, - policy, - concurrent_ios_per_device, - bw_limit_per_device): - """Set the rebalance I/O priority policy on the specified Storage Pool. - - :type storage_pool_id: str - :type policy: str - :type concurrent_ios_per_device: str - :type bw_limit_per_device: str - :rtype: dict - """ - - action = 'setRebalanceIoPriorityPolicy' - - params = { - 'policy': policy, - 'numOfConcurrentIosPerDevice': concurrent_ios_per_device, - 'bwLimitPerDeviceInKbps': bw_limit_per_device - } - - r, response = self.send_post_request(self.base_action_url, - action=action, - entity=self.entity, - entity_id=storage_pool_id, - params=params) - if r.status_code != requests.codes.ok: - msg = ( - f'Failed to set the rebalance I/O priority policy for PowerFlex {self.entity} ' - f'with id {storage_pool_id}. Error: {response}') - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) + # def query_selected_statistics(self, properties, ids=None): + # """Query PowerFlex storage pool statistics. - return self.get(entity_id=storage_pool_id) + # :type properties: list + # :type ids: list of storage pools IDs or None for all storage pools + # :rtype: dict + # """ - def set_fragmentation_enabled(self, storage_pool_id, enable_fragmentation): - """Enable/Disable the fragmentation on the specified Storage Pool. + # action = "querySelectedStatistics" - :type storage_pool_id: str - :type enable_fragmentation: bool - :rtype: dict - """ + # params = {'properties': properties} - action = 'disableFragmentation' - if enable_fragmentation: - action = 'enableFragmentation' + # if ids: + # params["ids"] = ids + # else: + # params["allIds"] = "" - r, response = self.send_post_request(self.base_action_url, - action=action, - entity=self.entity, - entity_id=storage_pool_id) - if r.status_code != requests.codes.ok: - msg = ( - f'Failed to enable/disable fragmentation for PowerFlex {self.entity} ' - f'with id {storage_pool_id}. Error: {response}') - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) + # return self._query_selected_statistics(action, params) diff --git a/setup.py b/setup.py index 38cc843..9020f14 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ setup( name='PyPowerFlex', - version='1.15.1', + version='2.0.0', description='Python library for Dell PowerFlex', author='Ansible Team at Dell', author_email='ansible.team@dell.com', From 722b1baf33c500b274bd13ca95ae44bd04b2f159 Mon Sep 17 00:00:00 2001 From: Luis Liu Date: Mon, 28 Jul 2025 10:52:48 +0800 Subject: [PATCH 05/15] Update base client Signed-off-by: Luis Liu --- PyPowerFlex/base_client.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/PyPowerFlex/base_client.py b/PyPowerFlex/base_client.py index 171eb16..e346cfe 100644 --- a/PyPowerFlex/base_client.py +++ b/PyPowerFlex/base_client.py @@ -422,6 +422,9 @@ def _delete_entity(self, entity_id, params=None): entity_id (str): The ID of the entity. params (dict, optional): Parameters for the entity. + Returns: + dict: The response from the API. + Raises: PowerFlexFailDeleting: If the entity fails to be deleted. """ @@ -437,6 +440,7 @@ def _delete_entity(self, entity_id, params=None): response) LOG.error(exc.message) raise exc + return response def _rename_entity(self, action, entity_id, params=None): """ @@ -448,7 +452,7 @@ def _rename_entity(self, action, entity_id, params=None): params (dict, optional): Parameters for the entity. Returns: - dict: The renamed entity. + dict: The response from the API. Raises: PowerFlexFailRenaming: If the entity fails to be renamed. @@ -463,8 +467,7 @@ def _rename_entity(self, action, entity_id, params=None): response) LOG.error(exc.message) raise exc - - return self.get(entity_id=entity_id) + return response def get(self, entity_id=None, filter_fields=None, fields=None): """ @@ -578,6 +581,7 @@ def _perform_entity_operation_based_on_action( self.entity, entity_id, action, response) LOG.error(exc.message) raise exc + return response def _query_selected_statistics(self, action, params=None): """ From d523086843813eeedbddaff54f2f58234b57f4e9 Mon Sep 17 00:00:00 2001 From: Yiming Bao Date: Tue, 29 Jul 2025 09:34:48 +0800 Subject: [PATCH 06/15] test change --- PyPowerFlex/__init__.py | 4 +- PyPowerFlex/constants.py | 45 ++++ PyPowerFlex/objects/common/__init__.py | 2 + .../objects/{gen1 => common}/utility.py | 37 ++- PyPowerFlex/objects/gen1/__init__.py | 2 - PyPowerFlex/objects/gen2/__init__.py | 2 - PyPowerFlex/objects/gen2/storage_node.py | 59 +---- PyPowerFlex/objects/gen2/utility.py | 69 ----- tests/common/__init__.py | 10 +- tests/common/test_host.py | 2 +- tests/common/test_sdc.py | 2 +- tests/common/test_sdt.py | 2 +- tests/common/test_system.py | 2 +- tests/{gen1 => common}/test_utility.py | 8 + tests/gen1/__init__.py | 234 +---------------- tests/gen1/test_acceleration_pool.py | 2 +- tests/gen1/test_base.py | 4 +- tests/gen1/test_deployment.py | 1 + tests/gen1/test_device.py | 2 +- tests/gen1/test_fault_set.py | 2 +- tests/gen1/test_firmware_repository.py | 2 +- tests/gen1/test_managed_device.py | 2 +- tests/gen1/test_protection_domain.py | 2 +- .../test_replication_consistency_group.py | 2 +- tests/gen1/test_replication_pair.py | 2 +- tests/gen1/test_sds.py | 2 +- tests/gen1/test_service_template.py | 2 +- tests/gen1/test_snapshot_policy.py | 2 +- tests/gen1/test_storage_pool.py | 2 +- tests/gen1/test_volume.py | 1 + tests/gen2/__init__.py | 235 ------------------ tests/gen2/test_storage_node.py | 184 +++----------- 32 files changed, 154 insertions(+), 775 deletions(-) rename PyPowerFlex/objects/{gen1 => common}/utility.py (80%) delete mode 100644 PyPowerFlex/objects/gen2/utility.py rename tests/{gen1 => common}/test_utility.py (89%) diff --git a/PyPowerFlex/__init__.py b/PyPowerFlex/__init__.py index a5a9e8e..98b4c22 100644 --- a/PyPowerFlex/__init__.py +++ b/PyPowerFlex/__init__.py @@ -125,6 +125,8 @@ def add_objects_common(self): self.__add_storage_entity('sdc', common.Sdc) self.__add_storage_entity('sdt', common.Sdt) self.__add_storage_entity('host', common.Host) + self.__add_storage_entity('utility', common.PowerFlexUtility) + def add_objects_gen1(self): self.__add_storage_entity('device', gen1.Device) @@ -139,7 +141,6 @@ def add_objects_gen1(self): self.__add_storage_entity('acceleration_pool', gen1.AccelerationPool) self.__add_storage_entity('volume', gen1.Volume) - self.__add_storage_entity('utility', gen1.PowerFlexUtility) self.__add_storage_entity( 'replication_consistency_group', gen1.ReplicationConsistencyGroup) @@ -155,4 +156,3 @@ def add_objects_gen2(self): 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('utility', gen2.PowerFlexUtility) diff --git a/PyPowerFlex/constants.py b/PyPowerFlex/constants.py index a71f05b..239c04d 100644 --- a/PyPowerFlex/constants.py +++ b/PyPowerFlex/constants.py @@ -17,6 +17,7 @@ # pylint: disable=too-few-public-methods + class StoragePoolConstants: """ This class holds constants related to StoragePool. @@ -369,3 +370,47 @@ class SnapshotPolicyConstants: "numOfExpiredButLockedSnapshots", "numOfSrcVols", "srcVolIds"] + + +class StorageNodeConstants: + """ + This class holds statistics constants related to StorageNode. + """ + DEFAULT_STATISTICS_PROPERTIES = [ + "storage_fe_write_latency", + "device_local_read_bandwidth", + "device_local_read_iops", + "device_remote_write_bandwidth", + "device_remote_write_iops", + "total_device_write_bandwidth", + "total_device_write_iops", + "avg_device_write_io_size", + "storage_fe_trim_latency", + "device_local_write_bandwidth", + "device_local_write_iops", + "avg_device_write_latency", + "storage_fe_trim_bandwidth", + "storage_fe_trim_iops", + "avg_fe_trim_io_size", + "avg_device_pmem_write_latency", + "device_remote_read_bandwidth", + "device_remote_read_iops", + "avg_device_pmem_read_latency", + "storage_fe_write_bandwidth", + "storage_fe_write_iops", + "avg_fe_write_io_size", + "storage_fe_read_bandwidth", + "storage_fe_read_iops", + "avg_fe_read_io_size", + "total_device_pmem_write_bandwidth", + "total_device_pmem_write_iops", + "avg_device_pmem_write_io_size", + "total_device_pmem_read_bandwidth", + "total_device_pmem_read_iops", + "avg_device_pmem_read_io_size", + "total_device_read_bandwidth", + "total_device_read_iops", + "avg_device_read_io_size", + "raw_total", + "storage_fe_read_latency", + "avg_device_read_latency"] diff --git a/PyPowerFlex/objects/common/__init__.py b/PyPowerFlex/objects/common/__init__.py index 61bc368..a4a378a 100644 --- a/PyPowerFlex/objects/common/__init__.py +++ b/PyPowerFlex/objects/common/__init__.py @@ -19,10 +19,12 @@ from PyPowerFlex.objects.common.host import Host from PyPowerFlex.objects.common.sdc import Sdc from PyPowerFlex.objects.common.sdt import Sdt +from PyPowerFlex.objects.common.utility import PowerFlexUtility __all__ = [ 'Sdc', 'Sdt', 'System', 'Host', + 'PowerFlexUtility', ] diff --git a/PyPowerFlex/objects/gen1/utility.py b/PyPowerFlex/objects/common/utility.py similarity index 80% rename from PyPowerFlex/objects/gen1/utility.py rename to PyPowerFlex/objects/common/utility.py index db60eea..a5e593c 100644 --- a/PyPowerFlex/objects/gen1/utility.py +++ b/PyPowerFlex/objects/common/utility.py @@ -22,7 +22,13 @@ from PyPowerFlex import base_client from PyPowerFlex import exceptions -from PyPowerFlex.constants import StoragePoolConstants, VolumeConstants, SnapshotPolicyConstants + +from PyPowerFlex.constants import ( + StoragePoolConstants, + VolumeConstants, + SnapshotPolicyConstants, + StorageNodeConstants +) LOG = logging.getLogger(__name__) @@ -30,6 +36,7 @@ class PowerFlexUtility(base_client.EntityRequest): "Utility class for PowerFlex" + def __init__(self, token, configuration): super().__init__(token, configuration) @@ -138,3 +145,31 @@ def get_statistics_for_all_snapshot_policies( raise exceptions.PowerFlexClientException(msg) return response + + def get_statistics_for_all_storage_nodes(self, ids=None, properties=None): + """list storage node statistics for PowerFlex 5.0+. + + :param ids: list + :param properties: list + :return: dict + """ + + default_properties = StorageNodeConstants.DEFAULT_STATISTICS_PROPERTIES + params = { + 'properties': default_properties if properties is None else properties} + if ids is not None: + params['ids'] = ids + + params['resource_type'] = 'storage_node' + + r, response = self.send_post_request(self.metrics_query_url, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to list storage node statistics for PowerFlex. " + f"Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response diff --git a/PyPowerFlex/objects/gen1/__init__.py b/PyPowerFlex/objects/gen1/__init__.py index c9076ce..5ebde98 100644 --- a/PyPowerFlex/objects/gen1/__init__.py +++ b/PyPowerFlex/objects/gen1/__init__.py @@ -23,7 +23,6 @@ from PyPowerFlex.objects.gen1.storage_pool import StoragePool from PyPowerFlex.objects.gen1.acceleration_pool import AccelerationPool from PyPowerFlex.objects.gen1.volume import Volume -from PyPowerFlex.objects.gen1.utility import PowerFlexUtility from PyPowerFlex.objects.gen1.replication_consistency_group import ReplicationConsistencyGroup from PyPowerFlex.objects.gen1.replication_pair import ReplicationPair from PyPowerFlex.objects.gen1.service_template import ServiceTemplate @@ -41,7 +40,6 @@ 'StoragePool', 'AccelerationPool', 'Volume', - 'PowerFlexUtility', 'ReplicationConsistencyGroup', 'ReplicationPair', 'ServiceTemplate', diff --git a/PyPowerFlex/objects/gen2/__init__.py b/PyPowerFlex/objects/gen2/__init__.py index 2d97233..7ed0cd3 100644 --- a/PyPowerFlex/objects/gen2/__init__.py +++ b/PyPowerFlex/objects/gen2/__init__.py @@ -18,11 +18,9 @@ from PyPowerFlex.objects.gen2.storage_node import StorageNode from PyPowerFlex.objects.gen2.protection_domain import ProtectionDomain from PyPowerFlex.objects.gen2.storage_pool import StoragePool -from PyPowerFlex.objects.gen2.utility import PowerFlexUtility __all__ = [ 'StorageNode', 'ProtectionDomain', 'StoragePool', - 'PowerFlexUtility', ] diff --git a/PyPowerFlex/objects/gen2/storage_node.py b/PyPowerFlex/objects/gen2/storage_node.py index dc3e8f9..7238541 100644 --- a/PyPowerFlex/objects/gen2/storage_node.py +++ b/PyPowerFlex/objects/gen2/storage_node.py @@ -29,7 +29,6 @@ LOG = logging.getLogger(__name__) - class StorageNodeIpRoles: """StorageNode ip roles.""" @@ -67,6 +66,7 @@ def entity(self): """ A class representing Storage Node client. """ + def add_ip(self, node_id, node_ip): """Add PowerFlex Storage Node IP-address. @@ -95,7 +95,7 @@ def add_ip(self, node_id, node_ip): def create(self, name, node_ips, - protection_domain_id=None, + protection_domain_id, ): """Create PowerFlex Storage Node. :type name: str @@ -112,7 +112,7 @@ def create(self, return self._create_entity(params) - def delete(self, node_id, force=None): + def delete(self, node_id): """Remove PowerFlex Storage Node. :type node_id: str @@ -120,11 +120,8 @@ def delete(self, node_id, force=None): :rtype: None """ - params = {"force": force} + return self._delete_entity(node_id) - return self._delete_entity(node_id, params) - - def rename(self, node_id, name): """Rename PowerFlex Storage Node. @@ -158,7 +155,7 @@ def remove_ip(self, node_id, ip): params=params) if r.status_code != requests.codes.ok: msg = f"Failed to remove IP from PowerFlex Storage Node " \ - f"with id {node_id}. Error: {response}" + f"with id {node_id}. Error: {response}" LOG.error(msg) raise exceptions.PowerFlexClientException(msg) @@ -196,49 +193,3 @@ def set_ip_role(self, node_id, ip, role): raise exceptions.PowerFlexClientException(msg) return self.get(entity_id=node_id) - - def set_performance_parameters(self, node_id, performance_profile): - """Set performance parameters for PowerFlex Storage Node. - - :type node_id: str - :type performance_profile: str - :rtype: dict - """ - - action = 'setNodePerformanceParameters' - - params = {"perfProfile": performance_profile} - - r, response = self.send_post_request(self.base_action_url, - action=action, - entity=self.entity, - entity_id=node_id, - params=params) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to set performance parameters for PowerFlex " - f"Storage Node with id {node_id}. Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return self.get(entity_id=node_id) - - # def query_selected_statistics(self, properties, ids=None): - # """Query PowerFlex Storage Node statistics. - - # :type properties: list - # :type ids: list of Storage Node IDs or None for all Storage Node - # :rtype: dict - # """ - - # action = "querySelectedStatistics" - - # params = {'properties': properties} - - # if ids: - # params["ids"] = ids - # else: - # params["allIds"] = "" - - # return self._query_selected_statistics(action, params) diff --git a/PyPowerFlex/objects/gen2/utility.py b/PyPowerFlex/objects/gen2/utility.py deleted file mode 100644 index c03fa10..0000000 --- a/PyPowerFlex/objects/gen2/utility.py +++ /dev/null @@ -1,69 +0,0 @@ -# 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. - -"""Utility module for PowerFlex.""" - -# pylint: disable=no-member,useless-parent-delegation -import logging - -import requests - -from PyPowerFlex import base_client -from PyPowerFlex import exceptions -from PyPowerFlex.constants import StoragePoolConstants, VolumeConstants, SnapshotPolicyConstants - - -LOG = logging.getLogger(__name__) - - -class PowerFlexUtility(base_client.EntityRequest): - "Utility class for PowerFlex" - def __init__(self, token, configuration): - super().__init__(token, configuration) - - # def get_statistics_for_all_storagepools(self, ids=None, properties=None): - # """list storagepool statistics for PowerFlex 5.0+. - - # :param ids: list - # :param properties: list - # :return: dict - # """ - - # action = 'querySelectedStatistics' - # version = self.get_api_version() - # default_properties = StoragePoolConstants.DEFAULT_STATISTICS_PROPERTIES - # if version != '3.5': - # default_properties = default_properties + \ - # StoragePoolConstants.DEFAULT_STATISTICS_PROPERTIES_ABOVE_3_5 - # params = { - # 'properties': default_properties if properties is None else properties} - # if ids is None: - # params['allIds'] = "" - # else: - # params['ids'] = ids - - # r, response = self.send_post_request(self.metrics_query_url, - # entity='StoragePool', - # action=action, - # params=params) - # if r.status_code != requests.codes.ok: - # msg = ( - # f"Failed to list storage pool statistics for PowerFlex. " - # f"Error: {response}" - # ) - # LOG.error(msg) - # raise exceptions.PowerFlexClientException(msg) - - # return response diff --git a/tests/common/__init__.py b/tests/common/__init__.py index 7440036..e65710b 100644 --- a/tests/common/__init__.py +++ b/tests/common/__init__.py @@ -18,6 +18,7 @@ # pylint: disable=too-many-instance-attributes,keyword-arg-before-vararg,broad-exception-raised,unused-argument import collections +import copy import contextlib import json import logging @@ -85,9 +86,10 @@ class PyPowerFlexTestCase(TestCase): @classmethod def version(cls, new_version): def decorator(subclass): - cls.DEFAULT_MOCK_RESPONSES[ - PyPowerFlexTestCase.RESPONSE_MODE.Valid - ][cls.VERSION_API_PATH] = new_version + subclass.DEFAULT_MOCK_RESPONSES = copy.deepcopy(cls.DEFAULT_MOCK_RESPONSES) + subclass.DEFAULT_MOCK_RESPONSES[ + cls.RESPONSE_MODE.Valid + ][cls.VERSION_API_PATH] = new_version return subclass return decorator @@ -105,7 +107,7 @@ def decorator(subclass): DEFAULT_MOCK_RESPONSES = { RESPONSE_MODE.Valid: { '/login': 'token', - VERSION_API_PATH: '3.5', + VERSION_API_PATH: '4.5', '/logout': '', }, RESPONSE_MODE.Invalid: { diff --git a/tests/common/test_host.py b/tests/common/test_host.py index 7c9ecaf..96857e1 100644 --- a/tests/common/test_host.py +++ b/tests/common/test_host.py @@ -18,7 +18,7 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -from tests.gen1 import PyPowerFlexTestCase +from tests.common import PyPowerFlexTestCase class TestHostClient(PyPowerFlexTestCase): diff --git a/tests/common/test_sdc.py b/tests/common/test_sdc.py index d5dc91d..c246074 100644 --- a/tests/common/test_sdc.py +++ b/tests/common/test_sdc.py @@ -18,7 +18,7 @@ # pylint: disable=invalid-name from PyPowerFlex import exceptions -from tests.gen1 import PyPowerFlexTestCase +from tests.common import PyPowerFlexTestCase class TestSdcClient(PyPowerFlexTestCase): diff --git a/tests/common/test_sdt.py b/tests/common/test_sdt.py index 734be89..98294c1 100644 --- a/tests/common/test_sdt.py +++ b/tests/common/test_sdt.py @@ -19,7 +19,7 @@ from PyPowerFlex import exceptions from PyPowerFlex.objects.common import sdt -from tests.gen1 import PyPowerFlexTestCase +from tests.common import PyPowerFlexTestCase class TestSdtClient(PyPowerFlexTestCase): diff --git a/tests/common/test_system.py b/tests/common/test_system.py index ed64a97..ffe7073 100644 --- a/tests/common/test_system.py +++ b/tests/common/test_system.py @@ -19,7 +19,7 @@ from PyPowerFlex import exceptions from PyPowerFlex.objects.common import system -from tests.gen1 import PyPowerFlexTestCase +from tests.common import PyPowerFlexTestCase class TestSystemClient(PyPowerFlexTestCase): diff --git a/tests/gen1/test_utility.py b/tests/common/test_utility.py similarity index 89% rename from tests/gen1/test_utility.py rename to tests/common/test_utility.py index b26d72d..7bf5701 100644 --- a/tests/gen1/test_utility.py +++ b/tests/common/test_utility.py @@ -39,6 +39,8 @@ def setUp(self): {}, '/types/Volume/instances/action/querySelectedStatistics': {}, + '/dtapi/rest/v1/metrics/query': + {}, } } @@ -69,3 +71,9 @@ def test_get_statistics_for_all_volumes_bad_status(self): with self.http_response_mode(self.RESPONSE_MODE.BadStatus): self.assertRaises(exceptions.PowerFlexClientException, self.client.utility.get_statistics_for_all_volumes) + + def test_get_statistics_for_all_storage_nodes(self): + """ + Test the get_statistics_for_all_storage_nodes method. + """ + self.client.utility.get_statistics_for_all_storage_nodes() diff --git a/tests/gen1/__init__.py b/tests/gen1/__init__.py index 74cebf5..4e768b5 100644 --- a/tests/gen1/__init__.py +++ b/tests/gen1/__init__.py @@ -1,233 +1 @@ -# 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. - -"""This module is used for the initialization of the test framework.""" - -# pylint: disable=too-many-instance-attributes,keyword-arg-before-vararg,broad-exception-raised,unused-argument - -import collections -import contextlib -import json -import logging -from unittest import mock -from unittest import TestCase - -import requests - -import PyPowerFlex -from PyPowerFlex import utils - - -class MockResponse(requests.Response): - """ - Mock HTTP Response. - - Defines http replies from mocked calls to do_request(). - """ - def __init__(self, content, status_code=200): - """ - Initialize a MockResponse. - - Args: - content (str or dict): The content of the response. - status_code (int): The status code of the response. - """ - super().__init__() - self._content = content - self.request = mock.MagicMock() - self.status_code = status_code - - def json(self, **kwargs): - """ - Return the content of the response as JSON. - - Args: - **kwargs: Additional keyword arguments. - - Returns: - dict: The content of the response. - """ - return self._content - - @property - def text(self): - """ - Return the content of the response as text. - - Returns: - str: The content of the response. - """ - if not isinstance(self._content, bytes): - return json.dumps(self._content) - return super().text - - -class PyPowerFlexTestCase(TestCase): - """ - Base test case for PyPowerFlex. - - Provides a mocked HTTP response for testing. - """ - RESPONSE_MODE = ( - collections.namedtuple('RESPONSE_MODE', 'Valid Invalid BadStatus') - (Valid='Valid', Invalid='Invalid', BadStatus='BadStatus') - ) - BAD_STATUS_RESPONSE = MockResponse( - { - 'errorCode': 500, - 'message': 'Test default bad status', - }, 500 - ) - MOCK_RESPONSES = {} - DEFAULT_MOCK_RESPONSES = { - RESPONSE_MODE.Valid: { - '/login': 'token', - '/version': '3.5', - '/logout': '', - }, - RESPONSE_MODE.Invalid: { - '/version': '2.5', - }, - RESPONSE_MODE.BadStatus: { - '/login': MockResponse( - { - 'errorCode': 1, - 'message': 'Test login bad status', - }, 400 - ), - '/version': MockResponse( - { - 'errorCode': 2, - 'message': 'Test version bad status', - }, 400 - ), - '/logout': MockResponse( - { - 'errorCode': 3, - 'message': 'Test logout bad status', - }, 400 - ) - } - } - __http_response_mode = RESPONSE_MODE.Valid - - def setUp(self): - """ - Set up the test case. - - Creates a PyPowerFlex client and sets up mocked HTTP responses. - """ - self.gateway_address = '1.2.3.4' - self.gateway_port = 443 - self.username = 'admin' - self.password = 'admin' - self.client = PyPowerFlex.PowerFlexClient(self.gateway_address, - self.gateway_port, - self.username, - self.password, - log_level=logging.DEBUG) - requests.request = self.get_mock_response - self.get_mock = self.mock_object(requests, - 'get', - side_effect=self.get_mock_response) - self.post_mock = self.mock_object(requests, - 'post', - side_effect=self.get_mock_response) - utils.is_version_3 = mock.MagicMock(return_value=True) - - def mock_object(self, obj, attr_name, *args, **kwargs): - """Use python mock to mock an object attribute. - - Mocks the specified objects attribute with the given value. - Automatically performs 'addCleanup' for the mock. - """ - patcher = mock.patch.object(obj, attr_name, *args, **kwargs) - result = patcher.start() - self.addCleanup(patcher.stop) - return result - - @contextlib.contextmanager - def http_response_mode(self, mode): - """ - Context manager for setting the HTTP response mode. - - Args: - mode: The HTTP response mode. - - Yields: - None. - """ - previous_response_mode, self.__http_response_mode = ( - self.__http_response_mode, mode - ) - yield - self.__http_response_mode = previous_response_mode - - def get_mock_response(self, url, request_url=None, mode=None, *args, **kwargs): - """ - Get a mock HTTP response. - - Args: - url (str): The URL of the request. - request_url (str): The URL of the request. - mode (str): The HTTP response mode. - *args: Additional arguments. - **kwargs: Additional keyword arguments. - - Returns: - requests.Response: The mocked HTTP response. - """ - if mode is None: - mode = self.__http_response_mode - - api_path = url.split('/api')[1] if ('/api' in url) else request_url.split('/api')[1] - try: - if api_path == "/login": - response = self.RESPONSE_MODE.Valid[0] - elif api_path == "/logout": - response = self.RESPONSE_MODE.Valid[2] - else: - response = self.MOCK_RESPONSES[mode][api_path] - except KeyError as e: - try: - response = self.DEFAULT_MOCK_RESPONSES[mode][api_path] - except KeyError: - if mode == self.RESPONSE_MODE.BadStatus: - response = self.BAD_STATUS_RESPONSE - else: - raise Exception( - f"Mock API Endpoint is not implemented: [{mode}]" - f"{api_path}" - ) from e - if not isinstance(response, MockResponse): - response = self._get_mock_response(response) - - response.request.url = url - response.request.body = kwargs.get('data') - return response - - def _get_mock_response(self, response): - """ - Returns a MockResponse object based on the given response. - - Args: - response (str): The response to be wrapped. - - Returns: - MockResponse: The mock response object. - """ - if "204" in str(response): - return MockResponse(response, 204) - return MockResponse(response, 200) +# \ No newline at end of file diff --git a/tests/gen1/test_acceleration_pool.py b/tests/gen1/test_acceleration_pool.py index d1fe4fe..70eeef3 100644 --- a/tests/gen1/test_acceleration_pool.py +++ b/tests/gen1/test_acceleration_pool.py @@ -21,7 +21,7 @@ from PyPowerFlex.objects.gen1 import acceleration_pool from tests.common import PyPowerFlexTestCase - +@PyPowerFlexTestCase.version('4.5') class TestAccelerationPoolClient(PyPowerFlexTestCase): """ Test class for the AccelerationPoolClient. diff --git a/tests/gen1/test_base.py b/tests/gen1/test_base.py index e3a54cd..968f6fc 100644 --- a/tests/gen1/test_base.py +++ b/tests/gen1/test_base.py @@ -19,9 +19,9 @@ from PyPowerFlex import exceptions from PyPowerFlex import utils -from tests.gen1 import PyPowerFlexTestCase - +from tests.common import PyPowerFlexTestCase +@PyPowerFlexTestCase.version('4.5') class TestBaseClient(PyPowerFlexTestCase): """ Test class for the BaseClient. diff --git a/tests/gen1/test_deployment.py b/tests/gen1/test_deployment.py index b782dbb..e81c245 100644 --- a/tests/gen1/test_deployment.py +++ b/tests/gen1/test_deployment.py @@ -20,6 +20,7 @@ from PyPowerFlex import exceptions from tests.common import PyPowerFlexTestCase +@PyPowerFlexTestCase.version('4.5') class TestDeploymentClient(PyPowerFlexTestCase): """ Test class for the DeploymentClient. diff --git a/tests/gen1/test_device.py b/tests/gen1/test_device.py index 96ea451..844f03d 100644 --- a/tests/gen1/test_device.py +++ b/tests/gen1/test_device.py @@ -21,7 +21,7 @@ from PyPowerFlex.objects.gen1.device import MediaType from tests.common import PyPowerFlexTestCase - +@PyPowerFlexTestCase.version('4.5') class TestDeviceClient(PyPowerFlexTestCase): """ Test class for DeviceClient. diff --git a/tests/gen1/test_fault_set.py b/tests/gen1/test_fault_set.py index b9a56d6..71686c0 100644 --- a/tests/gen1/test_fault_set.py +++ b/tests/gen1/test_fault_set.py @@ -20,7 +20,7 @@ from PyPowerFlex import exceptions from tests.common import PyPowerFlexTestCase - +@PyPowerFlexTestCase.version('4.5') class TestFaultSetClient(PyPowerFlexTestCase): """ Test class for the PowerFlex FaultSetClient. diff --git a/tests/gen1/test_firmware_repository.py b/tests/gen1/test_firmware_repository.py index 5a8d6b7..dbc1f8f 100644 --- a/tests/gen1/test_firmware_repository.py +++ b/tests/gen1/test_firmware_repository.py @@ -20,7 +20,7 @@ from PyPowerFlex import exceptions from tests.common import PyPowerFlexTestCase - +@PyPowerFlexTestCase.version('4.5') class TestFirmwareRepositoryClient(PyPowerFlexTestCase): """ Test class for FirmwareRepositoryClient. diff --git a/tests/gen1/test_managed_device.py b/tests/gen1/test_managed_device.py index bd7f056..5146260 100644 --- a/tests/gen1/test_managed_device.py +++ b/tests/gen1/test_managed_device.py @@ -20,7 +20,7 @@ from PyPowerFlex import exceptions from tests.common import PyPowerFlexTestCase - +@PyPowerFlexTestCase.version('4.5') class TestManagedDeviceClient(PyPowerFlexTestCase): """ Test class for the ManagedDeviceClient. diff --git a/tests/gen1/test_protection_domain.py b/tests/gen1/test_protection_domain.py index 63c529b..4d437e9 100644 --- a/tests/gen1/test_protection_domain.py +++ b/tests/gen1/test_protection_domain.py @@ -21,7 +21,7 @@ from PyPowerFlex.objects.gen1 import protection_domain from tests.common import PyPowerFlexTestCase - +@PyPowerFlexTestCase.version('4.5') class TestProtectionDomainClient(PyPowerFlexTestCase): """ Test class for the ProtectionDomainClient. diff --git a/tests/gen1/test_replication_consistency_group.py b/tests/gen1/test_replication_consistency_group.py index 40c4a10..6bc6314 100644 --- a/tests/gen1/test_replication_consistency_group.py +++ b/tests/gen1/test_replication_consistency_group.py @@ -20,7 +20,7 @@ from PyPowerFlex import exceptions from tests.common import PyPowerFlexTestCase - +@PyPowerFlexTestCase.version('4.5') class TestReplicationConsistencyGroupClient(PyPowerFlexTestCase): """ Tests for the ReplicationConsistencyGroupClient. diff --git a/tests/gen1/test_replication_pair.py b/tests/gen1/test_replication_pair.py index 517e9c3..b7d8e42 100644 --- a/tests/gen1/test_replication_pair.py +++ b/tests/gen1/test_replication_pair.py @@ -20,7 +20,7 @@ from PyPowerFlex import exceptions from tests.common import PyPowerFlexTestCase - +@PyPowerFlexTestCase.version('4.5') class TestReplicationPairClient(PyPowerFlexTestCase): """ Test class for the ReplicationPairClient. diff --git a/tests/gen1/test_sds.py b/tests/gen1/test_sds.py index 76f2502..7773554 100644 --- a/tests/gen1/test_sds.py +++ b/tests/gen1/test_sds.py @@ -21,7 +21,7 @@ from PyPowerFlex.objects.gen1 import sds from tests.common import PyPowerFlexTestCase - +@PyPowerFlexTestCase.version('4.5') class TestSdsClient(PyPowerFlexTestCase): """ Tests for the SdsClient class. diff --git a/tests/gen1/test_service_template.py b/tests/gen1/test_service_template.py index 11bd326..94db103 100644 --- a/tests/gen1/test_service_template.py +++ b/tests/gen1/test_service_template.py @@ -20,7 +20,7 @@ from PyPowerFlex import exceptions from tests.common import PyPowerFlexTestCase - +@PyPowerFlexTestCase.version('4.5') class TestServiceTemplateClient(PyPowerFlexTestCase): """ Test class for the ServiceTemplateClient. diff --git a/tests/gen1/test_snapshot_policy.py b/tests/gen1/test_snapshot_policy.py index 83617c7..cf00d82 100644 --- a/tests/gen1/test_snapshot_policy.py +++ b/tests/gen1/test_snapshot_policy.py @@ -21,7 +21,7 @@ from PyPowerFlex.objects.gen1 import snapshot_policy as sp from tests.common import PyPowerFlexTestCase - +@PyPowerFlexTestCase.version('4.5') class TestSnapshotPolicyClient(PyPowerFlexTestCase): """ Test class for snapshot policy client. diff --git a/tests/gen1/test_storage_pool.py b/tests/gen1/test_storage_pool.py index 5b8e3bf..fc0d6f3 100644 --- a/tests/gen1/test_storage_pool.py +++ b/tests/gen1/test_storage_pool.py @@ -23,7 +23,7 @@ from PyPowerFlex.objects.gen1.storage_pool import MediaType from tests.common import PyPowerFlexTestCase - +@PyPowerFlexTestCase.version('4.5') class TestStoragePoolClient(PyPowerFlexTestCase): """ Test class for the StoragePoolClient. diff --git a/tests/gen1/test_volume.py b/tests/gen1/test_volume.py index ef18f0c..f2b29b9 100644 --- a/tests/gen1/test_volume.py +++ b/tests/gen1/test_volume.py @@ -21,6 +21,7 @@ from PyPowerFlex.objects.gen1 import volume from tests.common import PyPowerFlexTestCase +@PyPowerFlexTestCase.version('4.5') class TestVolumeClient(PyPowerFlexTestCase): """ Test class for the volume client. diff --git a/tests/gen2/__init__.py b/tests/gen2/__init__.py index e0795f7..e69de29 100644 --- a/tests/gen2/__init__.py +++ b/tests/gen2/__init__.py @@ -1,235 +0,0 @@ -# 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. - -"""This module is used for the initialization of the test framework.""" - -# pylint: disable=too-many-instance-attributes,keyword-arg-before-vararg,broad-exception-raised,unused-argument - -import collections -import contextlib -import json -import logging -from unittest import mock -from unittest import TestCase - -import requests - -import PyPowerFlex -from PyPowerFlex import utils - - -class MockResponse(requests.Response): - """ - Mock HTTP Response. - - Defines http replies from mocked calls to do_request(). - """ - - def __init__(self, content, status_code=200): - """ - Initialize a MockResponse. - - Args: - content (str or dict): The content of the response. - status_code (int): The status code of the response. - """ - super().__init__() - self._content = content - self.request = mock.MagicMock() - self.status_code = status_code - - def json(self, **kwargs): - """ - Return the content of the response as JSON. - - Args: - **kwargs: Additional keyword arguments. - - Returns: - dict: The content of the response. - """ - return self._content - - @property - def text(self): - """ - Return the content of the response as text. - - Returns: - str: The content of the response. - """ - if not isinstance(self._content, bytes): - return json.dumps(self._content) - return super().text - - -class PyPowerFlexTestCase(TestCase): - """ - Base test case for PyPowerFlex. - - Provides a mocked HTTP response for testing. - """ - RESPONSE_MODE = ( - collections.namedtuple('RESPONSE_MODE', 'Valid Invalid BadStatus') - (Valid='Valid', Invalid='Invalid', BadStatus='BadStatus') - ) - BAD_STATUS_RESPONSE = MockResponse( - { - 'errorCode': 500, - 'message': 'Test default bad status', - }, 500 - ) - MOCK_RESPONSES = {} - DEFAULT_MOCK_RESPONSES = { - RESPONSE_MODE.Valid: { - '/login': 'token', - '/version': '5.0', - '/logout': '', - }, - RESPONSE_MODE.Invalid: { - '/version': '2.5', - }, - RESPONSE_MODE.BadStatus: { - '/login': MockResponse( - { - 'errorCode': 1, - 'message': 'Test login bad status', - }, 400 - ), - '/version': MockResponse( - { - 'errorCode': 2, - 'message': 'Test version bad status', - }, 400 - ), - '/logout': MockResponse( - { - 'errorCode': 3, - 'message': 'Test logout bad status', - }, 400 - ) - } - } - __http_response_mode = RESPONSE_MODE.Valid - - def setUp(self): - """ - Set up the test case. - - Creates a PyPowerFlex client and sets up mocked HTTP responses. - """ - self.gateway_address = '1.2.3.4' - self.gateway_port = 443 - self.username = 'admin' - self.password = 'admin' - self.client = PyPowerFlex.PowerFlexClient(self.gateway_address, - self.gateway_port, - self.username, - self.password, - log_level=logging.DEBUG) - requests.request = self.get_mock_response - self.get_mock = self.mock_object(requests, - 'get', - side_effect=self.get_mock_response) - self.post_mock = self.mock_object(requests, - 'post', - side_effect=self.get_mock_response) - utils.is_version_3 = mock.MagicMock(return_value=True) - - def mock_object(self, obj, attr_name, *args, **kwargs): - """Use python mock to mock an object attribute. - - Mocks the specified objects attribute with the given value. - Automatically performs 'addCleanup' for the mock. - """ - patcher = mock.patch.object(obj, attr_name, *args, **kwargs) - result = patcher.start() - self.addCleanup(patcher.stop) - return result - - @contextlib.contextmanager - def http_response_mode(self, mode): - """ - Context manager for setting the HTTP response mode. - - Args: - mode: The HTTP response mode. - - Yields: - None. - """ - previous_response_mode, self.__http_response_mode = ( - self.__http_response_mode, mode - ) - yield - self.__http_response_mode = previous_response_mode - - def get_mock_response(self, url, request_url=None, mode=None, *args, **kwargs): - """ - Get a mock HTTP response. - - Args: - url (str): The URL of the request. - request_url (str): The URL of the request. - mode (str): The HTTP response mode. - *args: Additional arguments. - **kwargs: Additional keyword arguments. - - Returns: - requests.Response: The mocked HTTP response. - """ - if mode is None: - mode = self.__http_response_mode - - api_path = url.split( - '/api')[1] if ('/api' in url) else request_url.split('/api')[1] - try: - if api_path == "/login": - response = self.RESPONSE_MODE.Valid[0] - elif api_path == "/logout": - response = self.RESPONSE_MODE.Valid[2] - else: - response = self.MOCK_RESPONSES[mode][api_path] - except KeyError as e: - try: - response = self.DEFAULT_MOCK_RESPONSES[mode][api_path] - except KeyError: - if mode == self.RESPONSE_MODE.BadStatus: - response = self.BAD_STATUS_RESPONSE - else: - raise Exception( - f"Mock API Endpoint is not implemented: [{mode}]" - f"{api_path}" - ) from e - if not isinstance(response, MockResponse): - response = self._get_mock_response(response) - - response.request.url = url - response.request.body = kwargs.get('data') - return response - - def _get_mock_response(self, response): - """ - Returns a MockResponse object based on the given response. - - Args: - response (str): The response to be wrapped. - - Returns: - MockResponse: The mock response object. - """ - if "204" in str(response): - return MockResponse(response, 204) - return MockResponse(response, 200) diff --git a/tests/gen2/test_storage_node.py b/tests/gen2/test_storage_node.py index 087e4bf..71e5316 100644 --- a/tests/gen2/test_storage_node.py +++ b/tests/gen2/test_storage_node.py @@ -18,7 +18,7 @@ # pylint: disable=invalid-name,too-many-public-methods from PyPowerFlex import exceptions -from PyPowerFlex.objects.gen2 import storage_node +from PyPowerFlex.objects.gen2.storage_node import StorageNode, StorageNodeIp, StorageNodeIpRoles from tests.common import PyPowerFlexTestCase @@ -27,6 +27,7 @@ class TestStorageNodeClient(PyPowerFlexTestCase): """ Tests for the StorageNodeClient class. """ + def setUp(self): """ Set up the test environment. @@ -36,38 +37,28 @@ def setUp(self): self.fake_node_id = '1' self.fake_sp_id = '1' self.fake_pd_id = '1' - self.fake_node_ips = [storage_node.StorageNodeIp( - '1.2.3.4', storage_node.StorageNodeIpRoles.storage_and_app)] + self.fake_node_ips = [StorageNodeIp( + '1.2.3.4', StorageNodeIpRoles.storage_and_app)] self.MOCK_RESPONSES = { self.RESPONSE_MODE.Valid: { - f'/types/{storage_node.StorageNode.entity}/instances': + '/types/Node/instances': {'id': self.fake_node_id}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_node_id}': + f'/instances/Node::{self.fake_node_id}': {'id': self.fake_node_id}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_node_id}/action/addIp': - {}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_node_id}/action/removestorage_node': + f'/instances/Node::{self.fake_node_id}/action/addIp': {}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_node_id}/relationships/Device': - [], - f'/instances/{storage_node.StorageNode.entity}::{self.fake_node_id}/action/setstorage_nodeName': + f'/instances/Node::{self.fake_node_id}/action/removeNode': {}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_node_id}/action/removestorage_nodeIp': + f'/instances/Node::{self.fake_node_id}/action/removeIp': {}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_node_id}/action/setstorage_nodeIpRole': + f'/instances/Node::{self.fake_node_id}/action/renameStorageNode': {}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_node_id}/action/setstorage_nodePort': + f'/instances/Node::{self.fake_node_id}/action/modifyIpRole': {}, - f'/instances/{storage_node.StorageNode.entity}::{self.fake_node_id}/action/setstorage_nodePerformanceParameters': - {}, - f'/types/{storage_node.StorageNode.entity}' - '/instances/action/querySelectedStatistics': { - self.fake_node_id: {'rfcacheFdReadTimeGreater5Sec': 0} - }, }, self.RESPONSE_MODE.Invalid: { - f'/types/{storage_node.StorageNode.entity}/instances': + '/types/Node/instances': {}, } } @@ -76,7 +67,8 @@ def test_storage_node_add_ip(self): """ Test the add_ip method of the storage_node client. """ - self.client.storage_node.add_ip(self.fake_node_id, self.fake_node_ips[0]) + self.client.storage_node.add_ip( + self.fake_node_id, self.fake_node_ips[0]) def test_storage_node_add_ip_bad_status(self): """ @@ -92,8 +84,11 @@ def test_storage_node_create(self): """ Test the create method of the storage_node client. """ - self.client.storage_node.create(protection_domain_id=self.fake_pd_id, - storage_node_ips=self.fake_node_ips) + self.client.storage_node.create( + name='fake_node_name', + node_ips=self.fake_node_ips, + protection_domain_id=self.fake_pd_id + ) def test_storage_node_create_bad_status(self): """ @@ -102,8 +97,10 @@ def test_storage_node_create_bad_status(self): with self.http_response_mode(self.RESPONSE_MODE.BadStatus): self.assertRaises(exceptions.PowerFlexFailCreating, self.client.storage_node.create, - protection_domain_id=self.fake_pd_id, - storage_node_ips=self.fake_node_ips) + name='fake_node_name', + node_ips=self.fake_node_ips, + protection_domain_id=self.fake_pd_id + ) def test_storage_node_create_no_id_in_response(self): """ @@ -112,8 +109,9 @@ def test_storage_node_create_no_id_in_response(self): with self.http_response_mode(self.RESPONSE_MODE.Invalid): self.assertRaises(KeyError, self.client.storage_node.create, - protection_domain_id=self.fake_pd_id, - storage_node_ips=self.fake_node_ips) + name='fake_node_name', + node_ips=[], + protection_domain_id=self.fake_pd_id) def test_storage_node_delete(self): """ @@ -130,21 +128,6 @@ def test_storage_node_delete_bad_status(self): self.client.storage_node.delete, self.fake_node_id) - def test_storage_node_get_devices(self): - """ - Test the get_devices method of the storage_node client. - """ - self.client.storage_node.get_devices(self.fake_node_id) - - def test_storage_node_get_devices_bad_status(self): - """ - Test the get_devices method of the storage_node client with a bad status. - """ - with self.http_response_mode(self.RESPONSE_MODE.BadStatus): - self.assertRaises(exceptions.PowerFlexClientException, - self.client.storage_node.get_devices, - self.fake_node_id) - def test_storage_node_rename(self): """ Test the rename method of the storage_node client. @@ -182,9 +165,8 @@ def test_storage_node_set_ip_role(self): Test the set_ip_role method. """ self.client.storage_node.set_ip_role(self.fake_node_id, - ip='1.2.3.4', - role=storage_node.storage_nodeIpRoles.sdc_only, - force=True) + ip='1.2.3.4', + role=StorageNodeIpRoles.storage) def test_storage_node_set_ip_role_bad_status(self): """ @@ -195,112 +177,4 @@ def test_storage_node_set_ip_role_bad_status(self): self.client.storage_node.set_ip_role, self.fake_node_id, ip='1.2.3.4', - role=storage_node.storage_nodeIpRoles.sdc_only, - force=True) - - def test_storage_node_set_port(self): - """ - Test the set_port method. - """ - self.client.storage_node.set_port(self.fake_node_id, storage_node_port=4443) - - def test_storage_node_set_port_bad_status(self): - """ - Test the set_port method with a bad status. - """ - with self.http_response_mode(self.RESPONSE_MODE.BadStatus): - self.assertRaises(exceptions.PowerFlexClientException, - self.client.storage_node.set_port, - self.fake_node_id, - storage_node_port=4443) - - def test_storage_node_set_rfcache_enabled(self): - """ - Test the set_rfcache_enabled method. - """ - self.client.storage_node.set_rfcache_enabled(self.fake_node_id, - rfcache_enabled=True) - - def test_storage_node_set_rfcache_enabled_bad_status(self): - """ - Test the set_rfcache_enabled method with a bad status. - """ - with self.http_response_mode(self.RESPONSE_MODE.BadStatus): - self.assertRaises(exceptions.PowerFlexClientException, - self.client.storage_node.set_rfcache_enabled, - self.fake_node_id, - rfcache_enabled=True) - - def test_storage_node_set_rmcache_enabled(self): - """ - Test the set_rmcache_enabled method. - """ - self.client.storage_node.set_rmcache_enabled(self.fake_node_id, - rmcache_enabled=True) - - def test_storage_node_set_rmcache_enabled_bad_status(self): - """ - Test the set_rmcache_enabled method with a bad status. - """ - with self.http_response_mode(self.RESPONSE_MODE.BadStatus): - self.assertRaises(exceptions.PowerFlexClientException, - self.client.storage_node.set_rmcache_enabled, - self.fake_node_id, - rmcache_enabled=True) - - def test_storage_node_set_rmcache_size(self): - """ - Test the set_rmcache_size method. - """ - self.client.storage_node.set_rmcache_size(self.fake_node_id, - rmcache_size=128) - - def test_storage_node_set_rmcache_size_bad_status(self): - """ - Test the set_rmcache_size method with a bad status. - """ - with self.http_response_mode(self.RESPONSE_MODE.BadStatus): - self.assertRaises(exceptions.PowerFlexClientException, - self.client.storage_node.set_rmcache_size, - self.fake_node_id, - rmcache_size=128) - - def test_storage_node_set_performance_parameters(self): - """ - Test the set_performance_parameters method. - """ - self.client.storage_node.set_performance_parameters( - self.fake_node_id, - performance_profile=storage_node.PerformanceProfile.highperformance) - - def test_storage_node_set_performance_parameters_bad_status(self): - """ - Test the set_performance_parameters method with a bad status. - """ - with self.http_response_mode(self.RESPONSE_MODE.BadStatus): - self.assertRaises( - exceptions.PowerFlexClientException, - self.client.storage_node.set_performance_parameters, - self.fake_node_id, - performance_profile=storage_node.PerformanceProfile.highperformance) - - def test_storage_node_query_selected_statistics(self): - """ - Test the query_selected_statistics method. - """ - ret = self.client.storage_node.query_selected_statistics( - properties=["rfcacheFdReadTimeGreater5Sec"] - ) - assert ret.get(self.fake_node_id).get( - "rfcacheFdReadTimeGreater5Sec") == 0 - - def test_storage_node_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.storage_node.query_selected_statistics, - properties=["rfcacheFdReadTimeGreater5Sec"], - ) + role=StorageNodeIpRoles.storage_and_app) From 4a5748dacfdc6e2ebaf4a8c9898de66243f2cd01 Mon Sep 17 00:00:00 2001 From: Yiming Bao Date: Tue, 29 Jul 2025 18:49:37 +0800 Subject: [PATCH 07/15] Dynamic __all__ --- PyPowerFlex/objects/common/__init__.py | 36 +++++++++++------ PyPowerFlex/objects/gen1/__init__.py | 53 +++++++++++--------------- PyPowerFlex/objects/gen2/__init__.py | 32 ++++++++++++---- 3 files changed, 71 insertions(+), 50 deletions(-) diff --git a/PyPowerFlex/objects/common/__init__.py b/PyPowerFlex/objects/common/__init__.py index a4a378a..c8b20f6 100644 --- a/PyPowerFlex/objects/common/__init__.py +++ b/PyPowerFlex/objects/common/__init__.py @@ -15,16 +15,28 @@ """This module contains the objects for interacting with the PowerFlex APIs.""" -from PyPowerFlex.objects.common.system import System -from PyPowerFlex.objects.common.host import Host -from PyPowerFlex.objects.common.sdc import Sdc -from PyPowerFlex.objects.common.sdt import Sdt -from PyPowerFlex.objects.common.utility import PowerFlexUtility +import os +import inspect +import importlib +import logging -__all__ = [ - 'Sdc', - 'Sdt', - 'System', - 'Host', - 'PowerFlexUtility', -] +from PyPowerFlex.base_client import EntityRequest +from PyPowerFlex import exceptions + +LOG = logging.getLogger(__name__) +__all__ = [] + +current_dir = os.path.dirname(__file__) +for filename in os.listdir(current_dir): + if filename.endswith(".py") and filename != "__init__.py": + module_name = filename[:-3] + try: + module = importlib.import_module(f"{__name__}.{module_name}") + for name, obj in inspect.getmembers(module, inspect.isclass): + if issubclass(obj, EntityRequest) and obj is not EntityRequest: + __all__.append(name) + globals()[name] = obj + except Exception as e: + msg = f"Failed to import module {module_name}: {e}" + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) diff --git a/PyPowerFlex/objects/gen1/__init__.py b/PyPowerFlex/objects/gen1/__init__.py index 5ebde98..c8b20f6 100644 --- a/PyPowerFlex/objects/gen1/__init__.py +++ b/PyPowerFlex/objects/gen1/__init__.py @@ -15,35 +15,28 @@ """This module contains the objects for interacting with the PowerFlex APIs.""" -from PyPowerFlex.objects.gen1.device import Device -from PyPowerFlex.objects.gen1.fault_set import FaultSet -from PyPowerFlex.objects.gen1.protection_domain import ProtectionDomain -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.acceleration_pool import AccelerationPool -from PyPowerFlex.objects.gen1.volume import Volume -from PyPowerFlex.objects.gen1.replication_consistency_group import ReplicationConsistencyGroup -from PyPowerFlex.objects.gen1.replication_pair import ReplicationPair -from PyPowerFlex.objects.gen1.service_template import ServiceTemplate -from PyPowerFlex.objects.gen1.managed_device import ManagedDevice -from PyPowerFlex.objects.gen1.deployment import Deployment -from PyPowerFlex.objects.gen1.firmware_repository import FirmwareRepository +import os +import inspect +import importlib +import logging +from PyPowerFlex.base_client import EntityRequest +from PyPowerFlex import exceptions -__all__ = [ - 'Device', - 'FaultSet', - 'ProtectionDomain', - 'Sds', - 'SnapshotPolicy', - 'StoragePool', - 'AccelerationPool', - 'Volume', - 'ReplicationConsistencyGroup', - 'ReplicationPair', - 'ServiceTemplate', - 'ManagedDevice', - 'Deployment', - 'FirmwareRepository', -] +LOG = logging.getLogger(__name__) +__all__ = [] + +current_dir = os.path.dirname(__file__) +for filename in os.listdir(current_dir): + if filename.endswith(".py") and filename != "__init__.py": + module_name = filename[:-3] + try: + module = importlib.import_module(f"{__name__}.{module_name}") + for name, obj in inspect.getmembers(module, inspect.isclass): + if issubclass(obj, EntityRequest) and obj is not EntityRequest: + __all__.append(name) + globals()[name] = obj + except Exception as e: + msg = f"Failed to import module {module_name}: {e}" + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) diff --git a/PyPowerFlex/objects/gen2/__init__.py b/PyPowerFlex/objects/gen2/__init__.py index 7ed0cd3..3fee1f2 100644 --- a/PyPowerFlex/objects/gen2/__init__.py +++ b/PyPowerFlex/objects/gen2/__init__.py @@ -15,12 +15,28 @@ """This module contains the objects for interacting with the PowerFlex 5.0+ APIs.""" -from PyPowerFlex.objects.gen2.storage_node import StorageNode -from PyPowerFlex.objects.gen2.protection_domain import ProtectionDomain -from PyPowerFlex.objects.gen2.storage_pool import StoragePool +import os +import inspect +import importlib +import logging -__all__ = [ - 'StorageNode', - 'ProtectionDomain', - 'StoragePool', -] +from PyPowerFlex.base_client import EntityRequest +from PyPowerFlex import exceptions + +LOG = logging.getLogger(__name__) +__all__ = [] + +current_dir = os.path.dirname(__file__) +for filename in os.listdir(current_dir): + if filename.endswith(".py") and filename != "__init__.py": + module_name = filename[:-3] + try: + module = importlib.import_module(f"{__name__}.{module_name}") + for name, obj in inspect.getmembers(module, inspect.isclass): + if issubclass(obj, EntityRequest) and obj is not EntityRequest: + __all__.append(name) + globals()[name] = obj + except Exception as e: + msg = f"Failed to import module {module_name}: {e}" + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) From 10a93a63dc291df15c535fce04456018888cc279 Mon Sep 17 00:00:00 2001 From: Yiming Bao Date: Wed, 30 Jul 2025 16:42:25 +0800 Subject: [PATCH 08/15] fix pylint & remove dynamic __all__ --- .github/workflows/pytest.yml | 2 +- PyPowerFlex/__init__.py | 9 +- PyPowerFlex/base_client.py | 10 +- PyPowerFlex/exceptions.py | 1 + PyPowerFlex/objects/common/__init__.py | 36 +-- PyPowerFlex/objects/gen1/__init__.py | 53 ++-- PyPowerFlex/objects/gen2/__init__.py | 32 +-- PyPowerFlex/objects/gen2/protection_domain.py | 136 +++++----- PyPowerFlex/objects/gen2/storage_node.py | 5 +- PyPowerFlex/objects/gen2/storage_pool.py | 238 ++++++++++-------- tests/common/__init__.py | 3 + tests/gen1/__init__.py | 1 - tests/gen2/test_storage_node.py | 2 +- 13 files changed, 276 insertions(+), 252 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index abe300c..57f1b53 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -36,7 +36,7 @@ jobs: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Run Unit Test and Generate report run: | - coverage run -m pytest -v tests/test_*.py + coverage run -m pytest -v tests/ - name: Upload Coverage to Codecov uses: codecov/codecov-action@v5 if: ${{ matrix.python-version == '3.12' }} diff --git a/PyPowerFlex/__init__.py b/PyPowerFlex/__init__.py index 98b4c22..e24b476 100644 --- a/PyPowerFlex/__init__.py +++ b/PyPowerFlex/__init__.py @@ -23,9 +23,9 @@ from PyPowerFlex import exceptions from PyPowerFlex import token from PyPowerFlex import utils -import PyPowerFlex.objects.common as common -import PyPowerFlex.objects.gen1 as gen1 -import PyPowerFlex.objects.gen2 as gen2 +from PyPowerFlex.objects import common +from PyPowerFlex.objects import gen1 +from PyPowerFlex.objects import gen2 __all__ = [ 'PowerFlexClient' @@ -121,6 +121,7 @@ def initialize(self): self.__is_initialized = True def add_objects_common(self): + """Add common objects here.""" self.__add_storage_entity('system', common.System) self.__add_storage_entity('sdc', common.Sdc) self.__add_storage_entity('sdt', common.Sdt) @@ -129,6 +130,7 @@ def add_objects_common(self): def add_objects_gen1(self): + """Add gen1 objects here.""" self.__add_storage_entity('device', gen1.Device) self.__add_storage_entity( 'fault_set', gen1.FaultSet) @@ -153,6 +155,7 @@ def add_objects_gen1(self): gen1.FirmwareRepository) def add_objects_gen2(self): + """Add gen2 objects here.""" self.__add_storage_entity('storage_node', gen2.StorageNode) self.__add_storage_entity('protection_domain', gen2.ProtectionDomain) self.__add_storage_entity('storage_pool', gen2.StoragePool) diff --git a/PyPowerFlex/base_client.py b/PyPowerFlex/base_client.py index e346cfe..dd935d5 100644 --- a/PyPowerFlex/base_client.py +++ b/PyPowerFlex/base_client.py @@ -21,10 +21,10 @@ import requests from requests.packages.urllib3.exceptions import InsecureRequestWarning - +from marshmallow import EXCLUDE, Schema from PyPowerFlex import exceptions from PyPowerFlex import utils -from marshmallow import EXCLUDE, INCLUDE, Schema + requests.packages.urllib3.disable_warnings(InsecureRequestWarning) LOG = logging.getLogger(__name__) @@ -615,12 +615,18 @@ def _query_selected_statistics(self, action, params=None): class BaseSchema(Schema): + """Base schema.""" + # pylint: disable=too-few-public-methods + def on_bind_field(self, field_name, field_obj): field_obj.data_key = camelcase(field_obj.data_key or field_name) class Meta: + """Meta class.""" unknown = EXCLUDE + def camelcase(s): + """Convert snake case to camel case.""" parts = iter(s.split("_")) return next(parts) + "".join(i.title() for i in parts) diff --git a/PyPowerFlex/exceptions.py b/PyPowerFlex/exceptions.py index e7c6d9a..77714b3 100644 --- a/PyPowerFlex/exceptions.py +++ b/PyPowerFlex/exceptions.py @@ -133,6 +133,7 @@ def __init__(self, entity, entity_id, action, response=None): def nonupdatable_exception(field, entity, entity_id=None): + """Return a PowerFlexClientException for non-updatable fields.""" msg = ( f'{field} cannot be updated after creation for PowerFlex {entity} {entity_id}' ) diff --git a/PyPowerFlex/objects/common/__init__.py b/PyPowerFlex/objects/common/__init__.py index c8b20f6..a4a378a 100644 --- a/PyPowerFlex/objects/common/__init__.py +++ b/PyPowerFlex/objects/common/__init__.py @@ -15,28 +15,16 @@ """This module contains the objects for interacting with the PowerFlex APIs.""" -import os -import inspect -import importlib -import logging +from PyPowerFlex.objects.common.system import System +from PyPowerFlex.objects.common.host import Host +from PyPowerFlex.objects.common.sdc import Sdc +from PyPowerFlex.objects.common.sdt import Sdt +from PyPowerFlex.objects.common.utility import PowerFlexUtility -from PyPowerFlex.base_client import EntityRequest -from PyPowerFlex import exceptions - -LOG = logging.getLogger(__name__) -__all__ = [] - -current_dir = os.path.dirname(__file__) -for filename in os.listdir(current_dir): - if filename.endswith(".py") and filename != "__init__.py": - module_name = filename[:-3] - try: - module = importlib.import_module(f"{__name__}.{module_name}") - for name, obj in inspect.getmembers(module, inspect.isclass): - if issubclass(obj, EntityRequest) and obj is not EntityRequest: - __all__.append(name) - globals()[name] = obj - except Exception as e: - msg = f"Failed to import module {module_name}: {e}" - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) +__all__ = [ + 'Sdc', + 'Sdt', + 'System', + 'Host', + 'PowerFlexUtility', +] diff --git a/PyPowerFlex/objects/gen1/__init__.py b/PyPowerFlex/objects/gen1/__init__.py index c8b20f6..5ebde98 100644 --- a/PyPowerFlex/objects/gen1/__init__.py +++ b/PyPowerFlex/objects/gen1/__init__.py @@ -15,28 +15,35 @@ """This module contains the objects for interacting with the PowerFlex APIs.""" -import os -import inspect -import importlib -import logging +from PyPowerFlex.objects.gen1.device import Device +from PyPowerFlex.objects.gen1.fault_set import FaultSet +from PyPowerFlex.objects.gen1.protection_domain import ProtectionDomain +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.acceleration_pool import AccelerationPool +from PyPowerFlex.objects.gen1.volume import Volume +from PyPowerFlex.objects.gen1.replication_consistency_group import ReplicationConsistencyGroup +from PyPowerFlex.objects.gen1.replication_pair import ReplicationPair +from PyPowerFlex.objects.gen1.service_template import ServiceTemplate +from PyPowerFlex.objects.gen1.managed_device import ManagedDevice +from PyPowerFlex.objects.gen1.deployment import Deployment +from PyPowerFlex.objects.gen1.firmware_repository import FirmwareRepository -from PyPowerFlex.base_client import EntityRequest -from PyPowerFlex import exceptions -LOG = logging.getLogger(__name__) -__all__ = [] - -current_dir = os.path.dirname(__file__) -for filename in os.listdir(current_dir): - if filename.endswith(".py") and filename != "__init__.py": - module_name = filename[:-3] - try: - module = importlib.import_module(f"{__name__}.{module_name}") - for name, obj in inspect.getmembers(module, inspect.isclass): - if issubclass(obj, EntityRequest) and obj is not EntityRequest: - __all__.append(name) - globals()[name] = obj - except Exception as e: - msg = f"Failed to import module {module_name}: {e}" - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) +__all__ = [ + 'Device', + 'FaultSet', + 'ProtectionDomain', + 'Sds', + 'SnapshotPolicy', + 'StoragePool', + 'AccelerationPool', + 'Volume', + 'ReplicationConsistencyGroup', + 'ReplicationPair', + 'ServiceTemplate', + 'ManagedDevice', + 'Deployment', + 'FirmwareRepository', +] diff --git a/PyPowerFlex/objects/gen2/__init__.py b/PyPowerFlex/objects/gen2/__init__.py index 3fee1f2..7ed0cd3 100644 --- a/PyPowerFlex/objects/gen2/__init__.py +++ b/PyPowerFlex/objects/gen2/__init__.py @@ -15,28 +15,12 @@ """This module contains the objects for interacting with the PowerFlex 5.0+ APIs.""" -import os -import inspect -import importlib -import logging +from PyPowerFlex.objects.gen2.storage_node import StorageNode +from PyPowerFlex.objects.gen2.protection_domain import ProtectionDomain +from PyPowerFlex.objects.gen2.storage_pool import StoragePool -from PyPowerFlex.base_client import EntityRequest -from PyPowerFlex import exceptions - -LOG = logging.getLogger(__name__) -__all__ = [] - -current_dir = os.path.dirname(__file__) -for filename in os.listdir(current_dir): - if filename.endswith(".py") and filename != "__init__.py": - module_name = filename[:-3] - try: - module = importlib.import_module(f"{__name__}.{module_name}") - for name, obj in inspect.getmembers(module, inspect.isclass): - if issubclass(obj, EntityRequest) and obj is not EntityRequest: - __all__.append(name) - globals()[name] = obj - except Exception as e: - msg = f"Failed to import module {module_name}: {e}" - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) +__all__ = [ + 'StorageNode', + 'ProtectionDomain', + 'StoragePool', +] diff --git a/PyPowerFlex/objects/gen2/protection_domain.py b/PyPowerFlex/objects/gen2/protection_domain.py index 47e72f7..94c31a2 100644 --- a/PyPowerFlex/objects/gen2/protection_domain.py +++ b/PyPowerFlex/objects/gen2/protection_domain.py @@ -45,6 +45,7 @@ class ProtectionDomainSchema(base_client.BaseSchema): + """Protection Domain schema.""" id = fields.Str( metadata={ "description": "Protection Domain Id", @@ -154,6 +155,7 @@ class ProtectionDomainSchema(base_client.BaseSchema): def load_protection_domain_schema(obj): + """Load protection domain schema.""" return ProtectionDomainSchema().load(obj) @@ -161,6 +163,7 @@ class ProtectionDomain(base_client.EntityRequest): """ A class representing Protection Domain client. """ + def list(self): """List PowerFlex protection domains. @@ -168,13 +171,13 @@ def list(self): """ return list(map(load_protection_domain_schema, self.get())) - def get_by_id(self, id): + def get_by_id(self, protection_domain_id): """Get PowerFlex protection domain. - :type id: str + :type protection_domain_id: str :rtype: dict """ - return load_protection_domain_schema(self.get(entity_id=id)) + return load_protection_domain_schema(self.get(entity_id=protection_domain_id)) def get_by_name(self, name): """Get PowerFlex protection domain. @@ -185,16 +188,15 @@ def get_by_name(self, name): result = self.get(filter_fields={'name': name}) if len(result) >= 1: return load_protection_domain_schema(result[0]) - else: - return None + return None - def delete(self, id): + def delete(self, protection_domain_id): """Remove PowerFlex protection domain. - :type id: str + :type protection_domain_id: str :rtype: None """ - return self._delete_entity(id) + return self._delete_entity(protection_domain_id) def create(self, pd): """Create PowerFlex protection domain. @@ -216,8 +218,10 @@ def update(self, pd, current_pd=None): :type pd: dict :rtype: dict """ - current_pd = current_pd if current_pd is not None else self.get_by_id(pd['id']) - pd = load_protection_domain_schema({**ProtectionDomainSchema().dump(current_pd), **pd}) + current_pd = current_pd if current_pd is not None else self.get_by_id( + pd['id']) + pd = load_protection_domain_schema( + {**ProtectionDomainSchema().dump(current_pd), **pd}) has_update = False @@ -245,24 +249,23 @@ def update(self, pd, current_pd=None): # TODO: unlimited, favorApplication "policy": "favorApplication", } - if pd['overall_concurrent_io_limit'] != current_pd['overall_concurrent_io_limit']: - policy['overallConcurrentIoLimit'] = pd['overall_concurrent_io_limit'] - if pd['bandwidth_limit_overall_ios'] != current_pd['bandwidth_limit_overall_ios']: - policy['bandwidthLimitOverallIos'] = pd['bandwidth_limit_overall_ios'] - if pd['bandwidth_limit_bg_dev_scanner'] != current_pd['bandwidth_limit_bg_dev_scanner']: - policy['bandwidthLimitBgDevScanner'] = pd['bandwidth_limit_bg_dev_scanner'] - if pd['bandwidth_limit_garbage_collector'] != current_pd['bandwidth_limit_garbage_collector']: - policy['bandwidthLimitGarbageCollector'] = pd['bandwidth_limit_garbage_collector'] - if pd['bandwidth_limit_singly_impacted_rebuild'] != current_pd['bandwidth_limit_singly_impacted_rebuild']: - policy['bandwidthLimitSinglyImpactedRebuild'] = pd['bandwidth_limit_singly_impacted_rebuild'] - if pd['bandwidth_limit_doubly_impacted_rebuild'] != current_pd['bandwidth_limit_doubly_impacted_rebuild']: - policy['bandwidthLimitDoublyImpactedRebuild'] = pd['bandwidth_limit_doubly_impacted_rebuild'] - if pd['bandwidth_limit_rebalance'] != current_pd['bandwidth_limit_rebalance']: - policy['bandwidthLimitRebalance'] = pd['bandwidth_limit_rebalance'] - if pd['bandwidth_limit_other'] != current_pd['bandwidth_limit_other']: - policy['bandwidthLimitOther'] = pd['bandwidth_limit_other'] - if pd['bandwidth_limit_node_network'] != current_pd['bandwidth_limit_node_network']: - policy['bandwidthLimitNodeNetwork'] = pd['bandwidth_limit_node_network'] + + field_map = { + 'overall_concurrent_io_limit': 'overallConcurrentIoLimit', + 'bandwidth_limit_overall_ios': 'bandwidthLimitOverallIos', + 'bandwidth_limit_bg_dev_scanner': 'bandwidthLimitBgDevScanner', + 'bandwidth_limit_garbage_collector': 'bandwidthLimitGarbageCollector', + 'bandwidth_limit_singly_impacted_rebuild': 'bandwidthLimitSinglyImpactedRebuild', + 'bandwidth_limit_doubly_impacted_rebuild': 'bandwidthLimitDoublyImpactedRebuild', + 'bandwidth_limit_rebalance': 'bandwidthLimitRebalance', + 'bandwidth_limit_other': 'bandwidthLimitOther', + 'bandwidth_limit_node_network': 'bandwidthLimitNodeNetwork', + } + + for py_key, api_key in field_map.items(): + if pd[py_key] != current_pd[py_key]: + policy[api_key] = pd[py_key] + if len(policy) > 1: has_update = True self.set_secondary_io_policy(pd['id'], policy) @@ -323,7 +326,7 @@ def inactivate(self, protection_domain_id, force=False): LOG.error(msg) raise exceptions.PowerFlexClientException(msg) - def enable_inflight_bandwidth_flow_control(self, id): + def enable_inflight_bandwidth_flow_control(self, protection_domain_id): """Enable inflight bandwidth flow control. :type id: str @@ -335,19 +338,19 @@ def enable_inflight_bandwidth_flow_control(self, id): r, response = self.send_post_request(self.base_action_url, action=action, entity=self.entity, - entity_id=id) + entity_id=protection_domain_id) if r.status_code != requests.codes.ok: msg = ( f"Failed to enable inflight bandwidth flow control in PowerFlex {self.entity} " - f"with id {id}. Error: {response}" + f"with id {protection_domain_id}. Error: {response}" ) LOG.error(msg) raise exceptions.PowerFlexClientException(msg) - def disable_inflight_bandwidth_flow_control(self, id): + def disable_inflight_bandwidth_flow_control(self, protection_domain_id): """Disable inflight bandwidth flow control. - :type id: str + :type protection_domain_id: str :rtype: None """ @@ -356,19 +359,19 @@ def disable_inflight_bandwidth_flow_control(self, id): r, response = self.send_post_request(self.base_action_url, action=action, entity=self.entity, - entity_id=id) + entity_id=protection_domain_id) if r.status_code != requests.codes.ok: msg = ( f"Failed to disable inflight bandwidth flow control in PowerFlex {self.entity} " - f"with id {id}. Error: {response}" + f"with id {protection_domain_id}. Error: {response}" ) LOG.error(msg) raise exceptions.PowerFlexClientException(msg) - def set_rebuild_enabled(self, id, enabled): + def set_rebuild_enabled(self, protection_domain_id, enabled): """Set rebuild state. - :type id: str + :type protection_domain_id: str :type enabled: bool :rtype: None """ @@ -381,20 +384,20 @@ def set_rebuild_enabled(self, id, enabled): r, response = self.send_post_request(self.base_action_url, action=action, entity=self.entity, - entity_id=id, + entity_id=protection_domain_id, params=params) if r.status_code != requests.codes.ok: msg = ( f"Failed to set rebuild state in PowerFlex {self.entity} " - f"with id {id}. Error: {response}" + f"with id {protection_domain_id}. Error: {response}" ) LOG.error(msg) raise exceptions.PowerFlexClientException(msg) - def set_rebalance_enabled(self, id, enabled): + def set_rebalance_enabled(self, protection_domain_id, enabled): """Set rebalance state. - :type id: str + :type protection_domain_id: str :type enabled: bool :rtype: None """ @@ -407,20 +410,20 @@ def set_rebalance_enabled(self, id, enabled): r, response = self.send_post_request(self.base_action_url, action=action, entity=self.entity, - entity_id=id, + entity_id=protection_domain_id, params=params) if r.status_code != requests.codes.ok: msg = ( f"Failed to set rebalance state in PowerFlex {self.entity} " - f"with id {id}. Error: {response}" + f"with id {protection_domain_id}. Error: {response}" ) LOG.error(msg) raise exceptions.PowerFlexClientException(msg) - def set_secondary_io_policy(self, id, policy): + def set_secondary_io_policy(self, protection_domain_id, policy): """Set secondary I/O policy. - :type id: str + :type protection_domain_id: str :type policy: Dict :rtype: None """ @@ -429,34 +432,31 @@ def set_secondary_io_policy(self, id, policy): params = { "policy": policy["policy"], } - if 'overallConcurrentIoLimit' in policy: - params["overallConcurrentIoLimit"] = policy["overallConcurrentIoLimit"] - if 'bandwidwith_limit_overall_ios' in policy: - params['bandwidthLimitOverallIos'] = policy['bandwidwith_limit_overall_ios'] - if 'bandwidth_limit_bg_dev_scanner' in policy: - params['bandwidthLimitBgDevScanner'] = policy['bandwidth_limit_bg_dev_scanner'] - if 'bandwidth_limit_garbage_collector' in policy: - params['bandwidthLimitGarbageCollector'] = policy['bandwidth_limit_garbage_collector'] - if 'bandwidth_limit_singly_impacted_rebuild' in policy: - params['bandwidthLimitSinglyImpactedRebuild'] = policy['bandwidth_limit_singly_impacted_rebuild'] - if 'bandwidth_limit_doubly_impacted_rebuild' in policy: - params['bandwidthLimitDoublyImpactedRebuild'] = policy['bandwidth_limit_doubly_impacted_rebuild'] - if 'bandwidth_limit_rebalance' in policy: - params['bandwidthLimitRebalance'] = policy['bandwidth_limit_rebalance'] - if 'bandwidth_limit_other' in policy: - params['bandwidthLimitOther'] = policy['bandwidth_limit_other'] - if 'bandwidth_limit_node_network' in policy: - params['bandwidthLimitNodeNetwork'] = policy['bandwidth_limit_node_network'] + field_map = { + 'overall_concurrent_io_limit': 'overallConcurrentIoLimit', + 'bandwidth_limit_overall_ios': 'bandwidthLimitOverallIos', + 'bandwidth_limit_bg_dev_scanner': 'bandwidthLimitBgDevScanner', + 'bandwidth_limit_garbage_collector': 'bandwidthLimitGarbageCollector', + 'bandwidth_limit_singly_impacted_rebuild': 'bandwidthLimitSinglyImpactedRebuild', + 'bandwidth_limit_doubly_impacted_rebuild': 'bandwidthLimitDoublyImpactedRebuild', + 'bandwidth_limit_rebalance': 'bandwidthLimitRebalance', + 'bandwidth_limit_other': 'bandwidthLimitOther', + 'bandwidth_limit_node_network': 'bandwidthLimitNodeNetwork', + } + + for py_key, api_key in field_map.items(): + if py_key in policy: + params[api_key] = policy[py_key] r, response = self.send_post_request(self.base_action_url, action=action, entity=self.entity, - entity_id=id, + entity_id=protection_domain_id, params=params) if r.status_code != requests.codes.ok: msg = ( f"Failed to set secondary I/O policy in PowerFlex {self.entity} " - f"with id {id}. Error: {response}" + f"with id {protection_domain_id}. Error: {response}" ) LOG.error(msg) raise exceptions.PowerFlexClientException(msg) @@ -478,19 +478,19 @@ def set_secondary_io_policy(self, id, policy): def get_storage_pools(self, protection_domain_id, filter_fields=None, - fields=None): + response_field=None): """Get related PowerFlex storage pools for protection domain. :type protection_domain_id: str :type filter_fields: dict - :type fields: list|tuple + :type response_field: list|tuple :rtype: list[dict] """ return self.get_related(protection_domain_id, 'StoragePool', filter_fields, - fields) + response_field) def rename(self, protection_domain_id, name): """Rename PowerFlex protection domain. diff --git a/PyPowerFlex/objects/gen2/storage_node.py b/PyPowerFlex/objects/gen2/storage_node.py index 7238541..b4043e4 100644 --- a/PyPowerFlex/objects/gen2/storage_node.py +++ b/PyPowerFlex/objects/gen2/storage_node.py @@ -56,6 +56,7 @@ def __init__(self, ip, role): class StorageNode(base_client.EntityRequest): + """PowerFlex Storage Node object.""" @property def entity(self): """ @@ -63,10 +64,6 @@ def entity(self): """ return "Node" - """ - A class representing Storage Node client. - """ - def add_ip(self, node_id, node_ip): """Add PowerFlex Storage Node IP-address. diff --git a/PyPowerFlex/objects/gen2/storage_pool.py b/PyPowerFlex/objects/gen2/storage_pool.py index 4d78ec3..8b87f66 100644 --- a/PyPowerFlex/objects/gen2/storage_pool.py +++ b/PyPowerFlex/objects/gen2/storage_pool.py @@ -20,7 +20,7 @@ import logging import requests -from marshmallow import INCLUDE, fields, validate, validates_schema, ValidationError +from marshmallow import fields, validate, ValidationError from PyPowerFlex import base_client, exceptions from PyPowerFlex.objects.gen2.protection_domain import ProtectionDomain @@ -29,11 +29,14 @@ def validate_over_provisioning_factor(value): + """Validate over provisioning factor.""" if value != 0 and (value < 100 or value > 10000): raise ValidationError("Not an valid value.") class StoragePoolSchema(base_client.BaseSchema): + """Storage Pool Schema.""" + id = fields.Str( metadata={ "description": "Storage Pool Id", @@ -44,26 +47,26 @@ class StoragePoolSchema(base_client.BaseSchema): metadata={ "description": "Storage Pool Name", "updatable": True, - } + }, ) protection_domain_id = fields.Str( required=True, metadata={ "description": "Protection Domain Id", "updatable": False, - } + }, ) device_group_id = fields.Str( required=True, metadata={ "description": "Device Group Id", "updatable": False, - } + }, ) wrc_device_group_id = fields.Str( metadata={ "description": "Device Group Id", - # TODO: + # TODO: # "updatable": False, } ) @@ -94,22 +97,28 @@ class StoragePoolSchema(base_client.BaseSchema): over_provisioning_factor = fields.Integer( validate=validate.And(validate_over_provisioning_factor), metadata={ - "description": "Over Provisioning Factor, range: 0, 100-10000, set 0 to disable over provisioning. Default: 0", - } + "description": ( + "Over Provisioning Factor, range: 0, 100-10000, " + "set 0 to disable over provisioning. Default: 0" + ), + }, ) physical_size_gb = fields.Integer( required=True, data_key="physicalSizeGB", metadata={ - "description": "Physical Size in GB, set -1 to use all available capacity. It only accepts larger value during update.", + "description": ( + "Physical Size in GB, set -1 to use all available capacity. " + "It only accepts larger value during update." + ), "updatable": True, - } + }, ) raw_size_gb = fields.Integer( data_key="rawSizeGB", metadata={ "description": "Raw Size in GB", - } + }, ) protection_scheme = fields.Str( required=True, @@ -117,31 +126,33 @@ class StoragePoolSchema(base_client.BaseSchema): metadata={ "description": "Protection Scheme: TwoPlusTwo/EightPlusTwo", "updatable": False, - } + }, ) compression_method = fields.Str( validate=validate.OneOf(["None", "Normal"]), metadata={ "description": "Compression Method: None/Normal. Default: Normal", "updatable": True, - } + }, ) zero_padding_enabled = fields.Boolean( metadata={ "description": "Zero padding enabled. Default: True", } ) - # @validates_schema # def validate_capacity_alert_threshold(self, data, **kwargs): # if data["capacity_alert_high_threshold"] >= data["capacity_alert_critical_threshold"]: - # raise ValidationError("capacity_alert_critical_threshold must be greater than capacity_alert_high_threshold") + # raise ValidationError( + # "capacity_alert_critical_threshold must be greater than " + # "capacity_alert_high_threshold" + # ) # class Meta: # unknown = INCLUDE - def load_storage_pool_schema(obj): + """Load storage pool schema.""" return StoragePoolSchema().load(obj) @@ -157,13 +168,13 @@ def list(self): """ return list(map(load_storage_pool_schema, self.get())) - def get_by_id(self, id): + def get_by_id(self, storage_pool_id): """Get PowerFlex storage pool. - :type id: str + :type storage_pool_id: str :rtype: dict """ - return load_storage_pool_schema(self.get(entity_id=id)) + return load_storage_pool_schema(self.get(entity_id=storage_pool_id)) def get_by_name(self, protion_domain_id, name): """Get PowerFlex storage pool. @@ -174,11 +185,10 @@ def get_by_name(self, protion_domain_id, name): """ pdo = ProtectionDomain(self.token, self.configuration) - result = pdo.get_storage_pools(protion_domain_id, filter_fields={'name': name}) + result = pdo.get_storage_pools(protion_domain_id, filter_fields={"name": name}) if len(result) >= 1: return load_storage_pool_schema(result[0]) - else: - return None + return None def create(self, sp): """Create PowerFlex storage pool. @@ -189,8 +199,8 @@ def create(self, sp): sp = load_storage_pool_schema(sp) params = { - "protectionDomainId": sp['protection_domain_id'], - "deviceGroupId": sp['device_group_id'], + "protectionDomainId": sp["protection_domain_id"], + "deviceGroupId": sp["device_group_id"], "gen": "EC", } @@ -212,7 +222,7 @@ def create(self, sp): params["physicalSizeGB"] = sp["physical_size_gb"] new_sp = load_storage_pool_schema(self._create_entity(params)) - sp['id'] = new_sp['id'] + sp["id"] = new_sp["id"] _, sp = self.update(StoragePoolSchema().dump(sp), new_sp) return sp @@ -223,51 +233,68 @@ def update(self, sp, current_sp=None): :type sp: dict :rtype: dict """ - current_sp = current_sp if current_sp is not None else self.get_by_id(sp['id']) + current_sp = current_sp if current_sp is not None else self.get_by_id(sp["id"]) sp = load_storage_pool_schema({**StoragePoolSchema().dump(current_sp), **sp}) - if sp['protection_domain_id'] != current_sp['protection_domain_id']: - e = exceptions.nonupdatable_exception("protection_domain_id", self.entity, sp['id']) + if sp["protection_domain_id"] != current_sp["protection_domain_id"]: + e = exceptions.nonupdatable_exception( + "protection_domain_id", self.entity, sp["id"] + ) LOG.error(e.message) raise e - if sp['device_group_id'] != current_sp['device_group_id']: - e = exceptions.nonupdatable_exception("device_group_id", self.entity, sp['id']) + if sp["device_group_id"] != current_sp["device_group_id"]: + e = exceptions.nonupdatable_exception( + "device_group_id", self.entity, sp["id"] + ) LOG.error(e.message) raise e - if sp['protection_scheme'] != current_sp['protection_scheme']: - e = exceptions.nonupdatable_exception("protection_scheme", self.entity, sp['id']) + if sp["protection_scheme"] != current_sp["protection_scheme"]: + e = exceptions.nonupdatable_exception( + "protection_scheme", self.entity, sp["id"] + ) LOG.error(e.message) raise e has_update = False - if sp['name'] != current_sp['name']: + if sp["name"] != current_sp["name"]: has_update = True - self.rename(sp['id'], sp['name']) + self.rename(sp["id"], sp["name"]) high_threshold = None critical_threshold = None - if sp['capacity_alert_high_threshold'] != current_sp['capacity_alert_high_threshold']: - high_threshold = sp['capacity_alert_high_threshold'] - if sp['capacity_alert_critical_threshold'] != current_sp['capacity_alert_critical_threshold']: - critical_threshold = sp['capacity_alert_critical_threshold'] + + if ( + sp["capacity_alert_high_threshold"] + != current_sp["capacity_alert_high_threshold"] + ): + high_threshold = sp["capacity_alert_high_threshold"] + + if ( + sp["capacity_alert_critical_threshold"] + != current_sp["capacity_alert_critical_threshold"] + ): + critical_threshold = sp["capacity_alert_critical_threshold"] + if high_threshold or critical_threshold: has_update = True - self.set_capacity_alert_thresholds(sp['id'], high_threshold, critical_threshold) + self.set_capacity_alert_thresholds( + sp["id"], high_threshold, critical_threshold + ) - if sp['over_provisioning_factor'] != current_sp['over_provisioning_factor']: + if sp["over_provisioning_factor"] != current_sp["over_provisioning_factor"]: has_update = True - self.set_over_provisioning_factor(sp['id'], sp['over_provisioning_factor']) + self.set_over_provisioning_factor(sp["id"], sp["over_provisioning_factor"]) - if sp['physical_size_gb'] != current_sp['physical_size_gb']: + if sp["physical_size_gb"] != current_sp["physical_size_gb"]: has_update = True - self.resize(sp['id'], sp['physical_size_gb']) + self.resize(sp["id"], sp["physical_size_gb"]) - if sp['compression_method'] != current_sp['compression_method']: + if sp["compression_method"] != current_sp["compression_method"]: has_update = True - self.set_compression_method(sp['id'], sp['compression_method']) + self.set_compression_method(sp["id"], sp["compression_method"]) - return has_update, self.get_by_id(sp['id']) + return has_update, self.get_by_id(sp["id"]) def delete(self, storage_pool_id): """Remove PowerFlex storage pool. @@ -348,12 +375,14 @@ def rename(self, storage_pool_id, name): :rtype: None """ - action = 'renameStoragePool' + action = "renameStoragePool" params = {"newName": name} self._rename_entity(action, storage_pool_id, params) - def set_capacity_alert_thresholds(self, storage_pool_id, high_threshold, critical_threshold): + def set_capacity_alert_thresholds( + self, storage_pool_id, high_threshold, critical_threshold + ): """Set the capacity alert thresholds for the specified Storage Pool. :type high_threshold: int @@ -361,22 +390,25 @@ def set_capacity_alert_thresholds(self, storage_pool_id, high_threshold, critica :rtype: None """ - action = 'setCapacityAlertThresholds' + action = "setCapacityAlertThresholds" params = { "capacityAlertHighThresholdPercent": high_threshold, - "capacityAlertCriticalThresholdPercent": critical_threshold + "capacityAlertCriticalThresholdPercent": critical_threshold, } - r, response = self.send_post_request(self.base_action_url, - action=action, - entity=self.entity, - entity_id=storage_pool_id, - params=params) + r, response = self.send_post_request( + self.base_action_url, + action=action, + entity=self.entity, + entity_id=storage_pool_id, + params=params, + ) if r.status_code != requests.codes.ok: msg = ( - f'Failed to set the capacity alert thresholds for PowerFlex {self.entity}' - f'with id {storage_pool_id}. Error: {response}') + f"Failed to set the capacity alert thresholds for PowerFlex {self.entity}" + f"with id {storage_pool_id}. Error: {response}" + ) LOG.error(msg) raise exceptions.PowerFlexClientException(msg) @@ -388,21 +420,22 @@ def set_over_provisioning_factor(self, storage_pool_id, over_provisioning_factor :rtype: None """ - action = 'setOverProvisioningFactor' + action = "setOverProvisioningFactor" - params = { - "overProvisioningFactor": over_provisioning_factor - } + params = {"overProvisioningFactor": over_provisioning_factor} - r, response = self.send_post_request(self.base_action_url, - action=action, - entity=self.entity, - entity_id=storage_pool_id, - params=params) + r, response = self.send_post_request( + self.base_action_url, + action=action, + entity=self.entity, + entity_id=storage_pool_id, + params=params, + ) if r.status_code != requests.codes.ok: msg = ( - f'Failed to set the over provisioning factor for PowerFlex {self.entity}' - f' with id {storage_pool_id}. Error: {response}') + f"Failed to set the over provisioning factor for PowerFlex {self.entity}" + f" with id {storage_pool_id}. Error: {response}" + ) LOG.error(msg) raise exceptions.PowerFlexClientException(msg) @@ -414,26 +447,25 @@ def resize(self, storage_pool_id, size_in_gb): :rtype: None """ - action = 'modifyStoragePoolSize' + action = "modifyStoragePoolSize" - params = { - "physicalSizeGB": size_in_gb - } + params = {"physicalSizeGB": size_in_gb} if size_in_gb == -1: - params = { - "useAllAvailableCapacity": True - } - - r, response = self.send_post_request(self.base_action_url, - action=action, - entity=self.entity, - entity_id=storage_pool_id, - params=params) + params = {"useAllAvailableCapacity": True} + + r, response = self.send_post_request( + self.base_action_url, + action=action, + entity=self.entity, + entity_id=storage_pool_id, + params=params, + ) if r.status_code != requests.codes.ok: msg = ( - f'Failed to modify the size for PowerFlex {self.entity}' - f' with id {storage_pool_id}. Error: {response}') + f"Failed to modify the size for PowerFlex {self.entity}" + f" with id {storage_pool_id}. Error: {response}" + ) LOG.error(msg) raise exceptions.PowerFlexClientException(msg) @@ -445,21 +477,22 @@ def set_compression_method(self, storage_pool_id, compression_method): :rtype: dict """ - action = 'modifyCompressionMethod' + action = "modifyCompressionMethod" - params = { - "compressionMethod": compression_method - } + params = {"compressionMethod": compression_method} - r, response = self.send_post_request(self.base_action_url, - action=action, - entity=self.entity, - entity_id=storage_pool_id, - params=params) + r, response = self.send_post_request( + self.base_action_url, + action=action, + entity=self.entity, + entity_id=storage_pool_id, + params=params, + ) if r.status_code != requests.codes.ok: msg = ( - f'Failed to set compression method for PowerFlex {self.entity} ' - f'with id {storage_pool_id}. Error: {response}') + f"Failed to set compression method for PowerFlex {self.entity} " + f"with id {storage_pool_id}. Error: {response}" + ) LOG.error(msg) raise exceptions.PowerFlexClientException(msg) @@ -471,19 +504,22 @@ def set_zero_padding_policy(self, storage_pool_id, zero_padding_enabled): :rtype: None """ - action = 'setZeroPaddingPolicy' + action = "setZeroPaddingPolicy" params = {"zeroPadEnabled": zero_padding_enabled} - r, response = self.send_post_request(self.base_action_url, - action=action, - entity=self.entity, - entity_id=storage_pool_id, - params=params) + r, response = self.send_post_request( + self.base_action_url, + action=action, + entity=self.entity, + entity_id=storage_pool_id, + params=params, + ) if r.status_code != requests.codes.ok: msg = ( - f'Failed to set Zero Padding policy for PowerFlex {self.entity} ' - f'with id {storage_pool_id}. Error: {response}') + f"Failed to set Zero Padding policy for PowerFlex {self.entity} " + f"with id {storage_pool_id}. Error: {response}" + ) LOG.error(msg) raise exceptions.PowerFlexClientException(msg) diff --git a/tests/common/__init__.py b/tests/common/__init__.py index e65710b..1fbe139 100644 --- a/tests/common/__init__.py +++ b/tests/common/__init__.py @@ -85,6 +85,9 @@ class PyPowerFlexTestCase(TestCase): @classmethod def version(cls, new_version): + """ + Decorator for mocking the version API version. + """ def decorator(subclass): subclass.DEFAULT_MOCK_RESPONSES = copy.deepcopy(cls.DEFAULT_MOCK_RESPONSES) subclass.DEFAULT_MOCK_RESPONSES[ diff --git a/tests/gen1/__init__.py b/tests/gen1/__init__.py index 4e768b5..e69de29 100644 --- a/tests/gen1/__init__.py +++ b/tests/gen1/__init__.py @@ -1 +0,0 @@ -# \ No newline at end of file diff --git a/tests/gen2/test_storage_node.py b/tests/gen2/test_storage_node.py index 71e5316..9dfd636 100644 --- a/tests/gen2/test_storage_node.py +++ b/tests/gen2/test_storage_node.py @@ -18,7 +18,7 @@ # pylint: disable=invalid-name,too-many-public-methods from PyPowerFlex import exceptions -from PyPowerFlex.objects.gen2.storage_node import StorageNode, StorageNodeIp, StorageNodeIpRoles +from PyPowerFlex.objects.gen2.storage_node import StorageNodeIp, StorageNodeIpRoles from tests.common import PyPowerFlexTestCase From bcff09934be85d626a0e9e67513d2fa3488c5730 Mon Sep 17 00:00:00 2001 From: Luis Liu Date: Wed, 30 Jul 2025 18:31:40 +0800 Subject: [PATCH 09/15] Add test case for protection domain & storage pool Signed-off-by: Luis Liu --- PyPowerFlex/objects/gen2/protection_domain.py | 88 +++-- PyPowerFlex/objects/gen2/storage_pool.py | 6 +- tests/gen2/test_protection_domain.py | 276 ++++++++++++++ tests/gen2/test_storage_pool.py | 339 ++++++++++++++++++ 4 files changed, 659 insertions(+), 50 deletions(-) create mode 100644 tests/gen2/test_protection_domain.py create mode 100644 tests/gen2/test_storage_pool.py diff --git a/PyPowerFlex/objects/gen2/protection_domain.py b/PyPowerFlex/objects/gen2/protection_domain.py index 94c31a2..8d0ced6 100644 --- a/PyPowerFlex/objects/gen2/protection_domain.py +++ b/PyPowerFlex/objects/gen2/protection_domain.py @@ -27,7 +27,7 @@ LOG = logging.getLogger(__name__) -# class LinkSchema(Schema): +# class LinkSchema(base_client.BaseSchema): # rel = fields.Str( # metadata={ # "description": "Rel", @@ -38,10 +38,6 @@ # "description": "Href", # } # ) -# def on_bind_field(self, field_name, field_obj): -# field_obj.data_key = camelcase(field_obj.data_key or field_name) -# class Meta: -# unknown = EXCLUDE class ProtectionDomainSchema(base_client.BaseSchema): @@ -206,9 +202,7 @@ def create(self, pd): """ pd = load_protection_domain_schema(pd) params = {"name": pd['name']} - new_pd = load_protection_domain_schema(self._create_entity(params)) - pd['id'] = new_pd['id'] _, pd = self.update(ProtectionDomainSchema().dump(pd), new_pd) return pd @@ -246,7 +240,9 @@ def update(self, pd, current_pd=None): # self.enable_inflight_bandwidth_flow_control(pd['id']) policy = { - # TODO: unlimited, favorApplication + # this value may change as the development gose on + # will fix in formal releases + # In additional, this value cannot be validated currently "policy": "favorApplication", } @@ -326,47 +322,47 @@ def inactivate(self, protection_domain_id, force=False): LOG.error(msg) raise exceptions.PowerFlexClientException(msg) - def enable_inflight_bandwidth_flow_control(self, protection_domain_id): - """Enable inflight bandwidth flow control. - - :type id: str - :rtype: None - """ - - action = 'enableInflightBandwidthFlowControl' - - r, response = self.send_post_request(self.base_action_url, - action=action, - entity=self.entity, - entity_id=protection_domain_id) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to enable inflight bandwidth flow control in PowerFlex {self.entity} " - f"with id {protection_domain_id}. Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - def disable_inflight_bandwidth_flow_control(self, protection_domain_id): - """Disable inflight bandwidth flow control. + # def enable_inflight_bandwidth_flow_control(self, id): + # """Enable inflight bandwidth flow control. - :type protection_domain_id: str - :rtype: None - """ + # :type id: str + # :rtype: None + # """ - action = 'disableInflightBandwidthFlowControl' + # action = 'enableInflightBandwidthFlowControl' + + # r, response = self.send_post_request(self.base_action_url, + # action=action, + # entity=self.entity, + # entity_id=id) + # if r.status_code != requests.codes.ok: + # msg = ( + # f"Failed to enable inflight bandwidth flow control in PowerFlex {self.entity} " + # f"with id {id}. Error: {response}" + # ) + # LOG.error(msg) + # raise exceptions.PowerFlexClientException(msg) + + # def disable_inflight_bandwidth_flow_control(self, id): + # """Disable inflight bandwidth flow control. + + # :type id: str + # :rtype: None + # """ - r, response = self.send_post_request(self.base_action_url, - action=action, - entity=self.entity, - entity_id=protection_domain_id) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to disable inflight bandwidth flow control in PowerFlex {self.entity} " - f"with id {protection_domain_id}. Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) + # action = 'disableInflightBandwidthFlowControl' + + # r, response = self.send_post_request(self.base_action_url, + # action=action, + # entity=self.entity, + # entity_id=id) + # if r.status_code != requests.codes.ok: + # msg = ( + # f"Failed to disable inflight bandwidth flow control in PowerFlex {self.entity} " + # f"with id {id}. Error: {response}" + # ) + # LOG.error(msg) + # raise exceptions.PowerFlexClientException(msg) def set_rebuild_enabled(self, protection_domain_id, enabled): """Set rebuild state. diff --git a/PyPowerFlex/objects/gen2/storage_pool.py b/PyPowerFlex/objects/gen2/storage_pool.py index 8b87f66..d796ebe 100644 --- a/PyPowerFlex/objects/gen2/storage_pool.py +++ b/PyPowerFlex/objects/gen2/storage_pool.py @@ -66,12 +66,11 @@ class StoragePoolSchema(base_client.BaseSchema): wrc_device_group_id = fields.Str( metadata={ "description": "Device Group Id", - # TODO: - # "updatable": False, } ) gen_type = fields.Str( - # required=True, # 5.0.0 only supports EC type + # 5.0.0 only supports EC type, so during creation, just pass EC to gen + # required=True, metadata={ "description": "Gen Type, EC or MIRRORING", } @@ -222,7 +221,6 @@ def create(self, sp): params["physicalSizeGB"] = sp["physical_size_gb"] new_sp = load_storage_pool_schema(self._create_entity(params)) - sp["id"] = new_sp["id"] _, sp = self.update(StoragePoolSchema().dump(sp), new_sp) return sp diff --git a/tests/gen2/test_protection_domain.py b/tests/gen2/test_protection_domain.py new file mode 100644 index 0000000..87156b9 --- /dev/null +++ b/tests/gen2/test_protection_domain.py @@ -0,0 +1,276 @@ +# 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 protection domain client.""" + +# pylint: disable=invalid-name,too-many-public-methods + +from PyPowerFlex import exceptions +from PyPowerFlex.objects.gen2.protection_domain import ProtectionDomain +from tests.common import PyPowerFlexTestCase + + +@PyPowerFlexTestCase.version('5.0') +class TestProtectionDomainClient(PyPowerFlexTestCase): + """ + Tests for the ProtectionDomainClient class. + """ + + def setUp(self): + """ + Set up the test environment. + """ + super().setUp() + self.client.initialize() + self.fake_pd_id = '1' + self.fake_pd_name = "pd-1" + pd = { + 'id': self.fake_pd_id, + 'name': self.fake_pd_name, + 'protectionDomainState': 'Active', + 'rebuildEnabled': False, + 'rebalanceEnabled': False, + 'overallConcurrentIoLimit': 0, + 'bandwidthLimitOverallIos': 0, + 'bandwidthLimitBgDevScanner': 0, + 'bandwidthLimitGarbageCollector': 0, + 'bandwidthLimitSinglyImpactedRebuild': 0, + 'bandwidthLimitDoublyImpactedRebuild': 0, + 'bandwidthLimitRebalance': 0, + 'bandwidthLimitOther': 0, + 'bandwidthLimitNodeNetwork': 0, + } + + self.MOCK_RESPONSES = { + self.RESPONSE_MODE.Valid: { + '/types/ProtectionDomain/instances': + pd, + f'/instances/ProtectionDomain::{self.fake_pd_id}': + pd, + f'/instances/ProtectionDomain::{self.fake_pd_id}/action/removeProtectionDomain': + {}, + f'/instances/ProtectionDomain::{self.fake_pd_id}/action/activateProtectionDomain': + {'id': self.fake_pd_id}, + f'/instances/ProtectionDomain::{self.fake_pd_id}/action/inactivateProtectionDomain': + {'id': self.fake_pd_id}, + f'/instances/ProtectionDomain::{self.fake_pd_id}/action/setProtectionDomainName': + {}, + f'/instances/ProtectionDomain::{self.fake_pd_id}/action/setRebuildEnabled': + {}, + f'/instances/ProtectionDomain::{self.fake_pd_id}/action/setRebalanceEnabled': + {}, + f'/instances/ProtectionDomain::{self.fake_pd_id}/action/setSecondaryIoPolicy': + {}, + f'/instances/ProtectionDomain::{self.fake_pd_id}/relationships/StoragePool': + [], + }, + self.RESPONSE_MODE.Invalid: { + '/types/ProtectionDomain/instances': + {}, + } + } + + def test_protection_domain_get_by_id(self): + """ + Test the get_by_id of a protection domain. + """ + self.client.protection_domain.get_by_id(self.fake_pd_id) + + def test_protection_domain_get_by_name(self): + """ + Test the get_by_name of a protection domain. + """ + self.client.protection_domain.get_by_name(self.fake_pd_name) + + def test_protection_domain_update(self): + """ + Test the update of a protection domain. + """ + pd = { + 'id': self.fake_pd_id, + 'name': "new_name", + 'protectionDomainState': 'Inactive', + 'rebuildEnabled': True, + 'rebalanceEnabled': True, + 'overallConcurrentIoLimit': 1, + 'bandwidthLimitOverallIos': 1, + 'bandwidthLimitBgDevScanner': 1, + 'bandwidthLimitGarbageCollector': 1, + 'bandwidthLimitSinglyImpactedRebuild': 1, + 'bandwidthLimitDoublyImpactedRebuild': 1, + 'bandwidthLimitRebalance': 1, + 'bandwidthLimitOther': 1, + 'bandwidthLimitNodeNetwork': 1, + } + self.client.protection_domain.update(pd) + + def test_protection_domain_create(self): + """ + Test the creation of a protection domain. + """ + self.client.protection_domain.create({"name": self.fake_pd_name}) + + def test_protection_domain_create_bad_status(self): + """ + Test the creation of a protection domain with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexFailCreating, + self.client.protection_domain.create, + {"name": self.fake_pd_name}) + + def test_protection_domain_delete(self): + """ + Test the deletion of a protection domain. + """ + self.client.protection_domain.delete(self.fake_pd_id) + + def test_protection_domain_delete_bad_status(self): + """ + Test the deletion of a protection domain with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexFailDeleting, + self.client.protection_domain.delete, + self.fake_pd_id) + + def test_protection_domain_activate(self): + """ + Test the activation of a protection domain. + """ + self.client.protection_domain.activate(self.fake_pd_id) + + def test_protection_domain_activate_bad_status(self): + """ + Test the activation of a protection domain with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.protection_domain.activate, + self.fake_pd_id) + + def test_protection_domain_inactivate(self): + """ + Test the inactivation of a protection domain. + """ + self.client.protection_domain.inactivate(self.fake_pd_id) + + def test_protection_domain_inactivate_bad_status(self): + """ + Test the inactivation of a protection domain with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.protection_domain.inactivate, + self.fake_pd_id) + + def test_protection_domain_rebuild(self): + """ + Test the rebuild of a protection domain. + """ + self.client.protection_domain.set_rebuild_enabled(self.fake_pd_id, False) + + def test_protection_domain_rebuild_bad_status(self): + """ + Test the rebuild of a protection domain with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.protection_domain.set_rebuild_enabled, + self.fake_pd_id, False) + + def test_protection_domain_rebalance(self): + """ + Test the rebalance of a protection domain. + """ + self.client.protection_domain.set_rebalance_enabled(self.fake_pd_id, False) + + def test_protection_domain_rebalance_bad_status(self): + """ + Test the rebalance of a protection domain with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.protection_domain.set_rebalance_enabled, + self.fake_pd_id, False) + + def test_protection_domain_set_secondary_io_policy(self): + """ + Test the set_secondary_io_policy of a protection domain. + """ + self.client.protection_domain.set_secondary_io_policy(self.fake_pd_id, { + 'policy': 'favorApplication', + 'overallConcurrentIoLimit': 0, + 'bandwidthLimitOverallIos': 0, + 'bandwidthLimitBgDevScanner': 0, + 'bandwidthLimitGarbageCollector': 0, + 'bandwidthLimitSinglyImpactedRebuild': 0, + 'bandwidthLimitDoublyImpactedRebuild': 0, + 'bandwidthLimitRebalance': 0, + 'bandwidthLimitOther': 0, + 'bandwidthLimitNodeNetwork': 0, + }) + + def test_protection_domain_set_secondary_io_policy_bad_status(self): + """ + Test the set_secondary_io_policy of a protection domain with a bad status. + """ + policy = { + 'policy': 'favorApplication', + 'overallConcurrentIoLimit': 0, + 'bandwidthLimitOverallIos': 0, + 'bandwidthLimitBgDevScanner': 0, + 'bandwidthLimitGarbageCollector': 0, + 'bandwidthLimitSinglyImpactedRebuild': 0, + 'bandwidthLimitDoublyImpactedRebuild': 0, + 'bandwidthLimitRebalance': 0, + 'bandwidthLimitOther': 0, + 'bandwidthLimitNodeNetwork': 0, + } + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.protection_domain.set_secondary_io_policy, + self.fake_pd_id, policy) + + def test_protection_domain_rename(self): + """ + Test the rename method of a protection domain. + """ + self.client.protection_domain.rename(self.fake_pd_id, name='new_name') + + def test_protection_domain_rename_bad_status(self): + """ + Test the rename method of a protection domain with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexFailRenaming, + self.client.protection_domain.rename, + self.fake_pd_id, + name='new_name') + + def test_protection_domain_get_storage_pools(self): + """ + Test the retrieval of storage pools for a protection domain. + """ + self.client.protection_domain.get_storage_pools(self.fake_pd_id) + + def test_protection_domain_get_storage_pools_bad_status(self): + """ + Test the retrieval of storage pools for a protection domain with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.protection_domain.get_storage_pools, + self.fake_pd_id) diff --git a/tests/gen2/test_storage_pool.py b/tests/gen2/test_storage_pool.py new file mode 100644 index 0000000..aad22a5 --- /dev/null +++ b/tests/gen2/test_storage_pool.py @@ -0,0 +1,339 @@ +# 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 storage pool client.""" + +# pylint: disable=invalid-name,too-many-public-methods + +from PyPowerFlex import exceptions +from PyPowerFlex.objects.gen2.storage_pool import StoragePool +from tests.common import PyPowerFlexTestCase + + +@PyPowerFlexTestCase.version('5.0') +class TestStoragePoolClient(PyPowerFlexTestCase): + """ + Tests for the StoragePoolClient class. + """ + + def setUp(self): + """ + Set up the test environment. + """ + super().setUp() + self.client.initialize() + self.fake_pd_id = '1' + self.fake_sp_id = '1' + self.fake_sp_name = "sp-1" + + sp = { + 'id': self.fake_sp_id, + 'name': self.fake_sp_name, + 'protectionDomainId': self.fake_pd_id, + 'deviceGroupId': "1", + 'wrcDeviceGroupId': "1", + 'genType': 'EC', + 'capacityAlertHighThreshold': 70, + 'capacityAlertCriticalThreshold': 90, + 'fragmentationEnabled': False, + 'overProvisioningFactor': 0, + 'physicalSizeGB': 10, + 'protectionScheme': 'TwoPlusTwo', + 'compressionMethod': 'None', + 'zeroPaddingEnabled': True, + } + + self.MOCK_RESPONSES = { + self.RESPONSE_MODE.Valid: { + '/types/StoragePool/instances': + sp, + f"/instances/StoragePool::{self.fake_sp_id}": + sp, + f'/instances/ProtectionDomain::{self.fake_pd_id}/relationships/StoragePool': + [sp], + f'/instances/StoragePool::{self.fake_sp_id}/action/removeStoragePool': + {}, + f'/instances/StoragePool::{self.fake_sp_id}/action/renameStoragePool': + {}, + f'/instances/StoragePool::{self.fake_sp_id}/action/setCapacityAlertThresholds': + {}, + f'/instances/StoragePool::{self.fake_sp_id}/action/setOverProvisioningFactor': + {}, + f'/instances/StoragePool::{self.fake_sp_id}/action/modifyStoragePoolSize': + {}, + f'/instances/StoragePool::{self.fake_sp_id}/action/modifyCompressionMethod': + {}, + f'/instances/StoragePool::{self.fake_sp_id}/action/setZeroPaddingPolicy': + {}, + }, + self.RESPONSE_MODE.Invalid: { + '/types/StoragePool/instances': + {}, + } + } + + def test_storage_pool_get_by_id(self): + """ + Test the get_by_id of a storage pool. + """ + self.client.storage_pool.get_by_id(self.fake_sp_id) + + def test_storage_pool_get_by_name(self): + """ + Test the get_by_name of a storage pool. + """ + self.client.storage_pool.get_by_name(self.fake_pd_id, self.fake_sp_name) + + def test_storage_pool_update(self): + """ + Test the update of a storage pool. + """ + sp = { + 'id': self.fake_sp_id, + 'name': "new_name", + 'capacityAlertHighThreshold': 80, + 'capacityAlertCriticalThreshold': 95, + 'fragmentationEnabled': True, + 'overProvisioningFactor': 1000, + 'physicalSizeGB': 2, + 'compressionMethod': 'Normal', + 'zeroPaddingEnabled': False, + } + self.client.storage_pool.update(sp) + + def test_storage_pool_update_bad_status(self): + """ + Test the update of a storage pool with a bad status. + """ + sp = { + 'id': self.fake_sp_id, + 'name': self.fake_sp_name, + 'protectionDomainId': "new_pd_id", + 'deviceGroupId': "1", + 'protectionScheme': 'TwoPlusTwo', + 'wrcDeviceGroupId': "1", + 'genType': 'EC', + 'capacityAlertHighThreshold': 70, + 'capacityAlertCriticalThreshold': 90, + 'fragmentationEnabled': False, + 'overProvisioningFactor': 0, + 'physicalSizeGB': 10, + 'compressionMethod': 'None', + 'zeroPaddingEnabled': True, + } + self.assertRaises(exceptions.PowerFlexClientException, + self.client.storage_pool.update, + sp) + + def test_storage_pool_update_bad_status_1(self): + """ + Test the update of a storage pool with a bad status. + """ + sp = { + 'id': self.fake_sp_id, + 'name': self.fake_sp_name, + 'protectionDomainId': self.fake_pd_id, + 'deviceGroupId': "2", + 'protectionScheme': 'TwoPlusTwo', + 'wrcDeviceGroupId': "1", + 'genType': 'EC', + 'capacityAlertHighThreshold': 70, + 'capacityAlertCriticalThreshold': 90, + 'fragmentationEnabled': False, + 'overProvisioningFactor': 0, + 'physicalSizeGB': 10, + 'compressionMethod': 'None', + 'zeroPaddingEnabled': True, + } + self.assertRaises(exceptions.PowerFlexClientException, + self.client.storage_pool.update, + sp) + + def test_storage_pool_update_bad_status_2(self): + """ + Test the update of a storage pool with a bad status. + """ + sp = { + 'id': self.fake_sp_id, + 'name': self.fake_sp_name, + 'protectionDomainId': self.fake_pd_id, + 'deviceGroupId': "1", + 'protectionScheme': 'EightPlusTwo', + 'wrcDeviceGroupId': "1", + 'genType': 'EC', + 'capacityAlertHighThreshold': 70, + 'capacityAlertCriticalThreshold': 90, + 'fragmentationEnabled': False, + 'overProvisioningFactor': 0, + 'physicalSizeGB': 10, + 'compressionMethod': 'None', + 'zeroPaddingEnabled': True, + } + self.assertRaises(exceptions.PowerFlexClientException, + self.client.storage_pool.update, + sp) + + def test_storage_pool_create(self): + """ + Test the creation of a storage pool. + """ + sp = { + 'id': self.fake_sp_id, + 'name': self.fake_sp_name, + 'protectionDomainId': self.fake_pd_id, + 'deviceGroupId': "1", + 'wrcDeviceGroupId': "1", + 'genType': 'EC', + 'capacityAlertHighThreshold': 70, + 'capacityAlertCriticalThreshold': 90, + 'fragmentationEnabled': False, + 'overProvisioningFactor': 0, + 'physicalSizeGB': 10, + 'protectionScheme': 'TwoPlusTwo', + 'compressionMethod': 'None', + 'zeroPaddingEnabled': True, + } + self.client.storage_pool.create(sp) + + def test_storage_pool_create_bad_status(self): + """ + Test the creation of a storage pool with a bad status. + """ + sp = { + 'id': self.fake_sp_id, + 'name': self.fake_sp_name, + 'protectionDomainId': self.fake_pd_id, + 'deviceGroupId': "1", + 'wrcDeviceGroupId': "1", + 'genType': 'EC', + 'capacityAlertHighThreshold': 70, + 'capacityAlertCriticalThreshold': 90, + 'fragmentationEnabled': False, + 'overProvisioningFactor': 0, + 'physicalSizeGB': 10, + 'protectionScheme': 'TwoPlusTwo', + 'compressionMethod': 'None', + 'zeroPaddingEnabled': True, + } + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexFailCreating, + self.client.storage_pool.create, + sp) + + def test_storage_pool_delete(self): + """ + Test the deletion of a storage pool. + """ + self.client.storage_pool.delete(self.fake_sp_id) + + def test_storage_pool_delete_bad_status(self): + """ + Test the deletion of a storage pool with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexFailDeleting, + self.client.storage_pool.delete, + self.fake_sp_id) + + def test_storage_pool_set_capacity_alert_thresholds(self): + """ + Test the set_capacity_alert_thresholds of a storage pool. + """ + self.client.storage_pool.set_capacity_alert_thresholds(self.fake_sp_id, 1, 2) + + def test_storage_pool_set_capacity_alert_thresholds_bad_status(self): + """ + Test the set_capacity_alert_thresholds of a storage pool with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.storage_pool.set_capacity_alert_thresholds, + self.fake_sp_id, 1, 2) + + def test_storage_pool_set_over_provisioning_factor(self): + """ + Test the set_over_provisioning_factor of a storage pool. + """ + self.client.storage_pool.set_over_provisioning_factor(self.fake_sp_id, 0) + + def test_storage_pool_set_over_provisioning_factor_bad_status(self): + """ + Test the set_over_provisioning_factor of a storage pool with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.storage_pool.set_over_provisioning_factor, + self.fake_sp_id, 0) + + def test_storage_pool_resize(self): + """ + Test the resize of a storage pool. + """ + self.client.storage_pool.resize(self.fake_sp_id, 1) + + def test_storage_pool_resize_bad_status(self): + """ + Test the resize of a storage pool with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.storage_pool.resize, + self.fake_sp_id, 1) + + def test_storage_pool_set_compression_method(self): + """ + Test the set_compression_method of a storage pool. + """ + self.client.storage_pool.set_compression_method(self.fake_sp_id, "None") + + def test_storage_pool_set_compression_method_bad_status(self): + """ + Test the set_compression_method of a storage pool with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.storage_pool.set_compression_method, + self.fake_sp_id, "None") + + def test_storage_pool_set_zero_padding_policy(self): + """ + Test the set_zero_padding_policy of a storage pool. + """ + self.client.storage_pool.set_zero_padding_policy(self.fake_sp_id, False) + + def test_storage_pool_set_zero_padding_policy_bad_status(self): + """ + Test the set_zero_padding_policy of a storage pool with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexClientException, + self.client.storage_pool.set_zero_padding_policy, + self.fake_sp_id, False) + + def test_storage_pool_rename(self): + """ + Test the rename method of a storage pool. + """ + self.client.storage_pool.rename(self.fake_sp_id, name='new_name') + + def test_storage_pool_rename_bad_status(self): + """ + Test the rename method of a storage pool with a bad status. + """ + with self.http_response_mode(self.RESPONSE_MODE.BadStatus): + self.assertRaises(exceptions.PowerFlexFailRenaming, + self.client.storage_pool.rename, + self.fake_sp_id, + name='new_name') From 982f7b42a34ccc26b70d4a0614687da7ccbaa549 Mon Sep 17 00:00:00 2001 From: Yiming Bao Date: Wed, 30 Jul 2025 22:37:45 +0800 Subject: [PATCH 10/15] add query_metrics that is common method for metric query rename token.py to powerflex_token.py to avoid python native token conflict --- PyPowerFlex/__init__.py | 4 +- PyPowerFlex/base_client.py | 16 ++++++-- PyPowerFlex/constants.py | 2 +- PyPowerFlex/objects/common/utility.py | 26 ++++++++---- PyPowerFlex/{token.py => powerflex_token.py} | 2 +- tests/common/__init__.py | 42 ++++++++++++++++++-- tests/gen2/test_protection_domain.py | 13 +++--- tests/gen2/test_storage_pool.py | 37 ++++++++++------- 8 files changed, 102 insertions(+), 40 deletions(-) rename PyPowerFlex/{token.py => powerflex_token.py} (98%) diff --git a/PyPowerFlex/__init__.py b/PyPowerFlex/__init__.py index e24b476..630d001 100644 --- a/PyPowerFlex/__init__.py +++ b/PyPowerFlex/__init__.py @@ -21,7 +21,7 @@ from PyPowerFlex import configuration from PyPowerFlex import exceptions -from PyPowerFlex import token +from PyPowerFlex import powerflex_token from PyPowerFlex import utils from PyPowerFlex.objects import common from PyPowerFlex.objects import gen1 @@ -84,7 +84,7 @@ def __init__(self, certificate_path, timeout, log_level) - self.token = token.Token() + self.token = powerflex_token.PowerFlexToken() self.__is_initialized = False def __getattr__(self, item): diff --git a/PyPowerFlex/base_client.py b/PyPowerFlex/base_client.py index dd935d5..d0575a9 100644 --- a/PyPowerFlex/base_client.py +++ b/PyPowerFlex/base_client.py @@ -111,13 +111,14 @@ def get_auth_headers(self, request_type=None): 'content-type': 'application/json' } - def send_request(self, method, url, params=None, **url_params): + def send_request(self, method, url, params=None, use_base_url=True, **url_params): """ Send a request to the PowerFlex API. Args: method (str): The HTTP method. url (str): The URL. + use_base_url (bool, optional): Whether to use the base URL. Defaults to True. params (dict): The parameters. url_params (dict): The URL parameters. @@ -125,7 +126,12 @@ def send_request(self, method, url, params=None, **url_params): Response: The response object. """ params = params or {} - request_url = f"{self.base_url}{url.format(**url_params)}" + use_base_url = True if use_base_url is None else use_base_url + if use_base_url: + request_url = f"{self.base_url}{url.format(**url_params)}" + else: + request_url = f"{self.base_url.removesuffix('/api')}{url.format(**url_params)}" + version = self.login() request_params = { 'headers': self.get_auth_headers(method), @@ -158,19 +164,21 @@ def send_get_request(self, url, params=None, **url_params): response = self.send_request(self.GET, url, params, **url_params) return response, response.json() - def send_post_request(self, url, params=None, **url_params): + def send_post_request(self, url, use_base_url=True, params=None, **url_params): """ Send a POST request to the PowerFlex API. Args: url (str): The URL. + use_base_url (bool, optional): Whether to use the base URL. Defaults to True. params (dict): The parameters. url_params (dict): The URL parameters. Returns: tuple: The response object and the response content. """ - response = self.send_request(self.POST, url, params, **url_params) + response = self.send_request( + self.POST, url, params, use_base_url, ** url_params) return response, response.json() def send_put_request(self, url, params=None, **url_params): diff --git a/PyPowerFlex/constants.py b/PyPowerFlex/constants.py index 239c04d..891de6b 100644 --- a/PyPowerFlex/constants.py +++ b/PyPowerFlex/constants.py @@ -376,7 +376,7 @@ class StorageNodeConstants: """ This class holds statistics constants related to StorageNode. """ - DEFAULT_STATISTICS_PROPERTIES = [ + DEFAULT_STATISTICS_METRICS = [ "storage_fe_write_latency", "device_local_read_bandwidth", "device_local_read_iops", diff --git a/PyPowerFlex/objects/common/utility.py b/PyPowerFlex/objects/common/utility.py index a5e593c..5bd1991 100644 --- a/PyPowerFlex/objects/common/utility.py +++ b/PyPowerFlex/objects/common/utility.py @@ -146,27 +146,39 @@ def get_statistics_for_all_snapshot_policies( return response - def get_statistics_for_all_storage_nodes(self, ids=None, properties=None): + def get_statistics_for_all_storage_nodes(self, ids=None, metrics=None): """list storage node statistics for PowerFlex 5.0+. :param ids: list - :param properties: list + :param metrics: list + :return: dict + """ + metrics = metrics or StorageNodeConstants.DEFAULT_STATISTICS_METRICS + return self.query_metrics('storage_node', ids, metrics) + + def query_metrics(self, resource_type, ids=None, metrics=None): + """Query PowerFlex resource metrics. + + :param resource_type: str + :param ids: list + :param metrics: list :return: dict """ - default_properties = StorageNodeConstants.DEFAULT_STATISTICS_PROPERTIES params = { - 'properties': default_properties if properties is None else properties} + 'resource_type': resource_type + } if ids is not None: params['ids'] = ids - - params['resource_type'] = 'storage_node' + if metrics is not None: + params['metrics'] = metrics r, response = self.send_post_request(self.metrics_query_url, + use_base_url=False, params=params) if r.status_code != requests.codes.ok: msg = ( - f"Failed to list storage node statistics for PowerFlex. " + f"Failed to query {resource_type} statistics. " f"Error: {response}" ) LOG.error(msg) diff --git a/PyPowerFlex/token.py b/PyPowerFlex/powerflex_token.py similarity index 98% rename from PyPowerFlex/token.py rename to PyPowerFlex/powerflex_token.py index 0538423..c747401 100644 --- a/PyPowerFlex/token.py +++ b/PyPowerFlex/powerflex_token.py @@ -15,7 +15,7 @@ """This module is used for the management of token.""" -class Token: +class PowerFlexToken: """ A class to manage a token. """ diff --git a/tests/common/__init__.py b/tests/common/__init__.py index 1fbe139..7e4b68b 100644 --- a/tests/common/__init__.py +++ b/tests/common/__init__.py @@ -37,6 +37,7 @@ class MockResponse(requests.Response): Defines http replies from mocked calls to do_request(). """ + def __init__(self, content, status_code=200): """ Initialize a MockResponse. @@ -88,12 +89,15 @@ def version(cls, new_version): """ Decorator for mocking the version API version. """ + def decorator(subclass): - subclass.DEFAULT_MOCK_RESPONSES = copy.deepcopy(cls.DEFAULT_MOCK_RESPONSES) + subclass.DEFAULT_MOCK_RESPONSES = copy.deepcopy( + cls.DEFAULT_MOCK_RESPONSES) subclass.DEFAULT_MOCK_RESPONSES[ - cls.RESPONSE_MODE.Valid - ][cls.VERSION_API_PATH] = new_version + cls.RESPONSE_MODE.Valid + ][cls.VERSION_API_PATH] = new_version return subclass + return decorator RESPONSE_MODE = ( @@ -191,6 +195,36 @@ def http_response_mode(self, mode): yield self.__http_response_mode = previous_response_mode + def extract_path_segment(self, url, request_url): + """ + Return the REST path from a URL, removing the domain and optional '/api' prefix. + If `url` lacks domain info, fallback to `request_url`. Ensures output starts with '/'. + """ + + def strip_domain(u): + """ + Strip scheme and domain from a full URL, keeping only the path. + """ + if '://' in u: + parts = u.split('://', 1)[-1].split('/', 1) + return '/' + parts[1] if len(parts) > 1 else '/' + if u.startswith('/'): + return u + parts = request_url.split('://', 1)[-1].split('/', 1) + return '/' + parts[1] if len(parts) > 1 else '/' + + path = strip_domain(url) + + if path.startswith('/api/'): + path = path[4:] # remove /api + elif path == '/api': + path = '/' + + if not path.startswith('/'): + path = '/' + path + + return path + def get_mock_response(self, url, request_url=None, mode=None, *args, **kwargs): """ Get a mock HTTP response. @@ -208,7 +242,7 @@ def get_mock_response(self, url, request_url=None, mode=None, *args, **kwargs): if mode is None: mode = self.__http_response_mode - api_path = url.split('/api')[1] if ('/api' in url) else request_url.split('/api')[1] + api_path = self.extract_path_segment(url, request_url) try: if api_path == "/login": response = self.RESPONSE_MODE.Valid[0] diff --git a/tests/gen2/test_protection_domain.py b/tests/gen2/test_protection_domain.py index 87156b9..f4859e1 100644 --- a/tests/gen2/test_protection_domain.py +++ b/tests/gen2/test_protection_domain.py @@ -18,7 +18,6 @@ # pylint: disable=invalid-name,too-many-public-methods from PyPowerFlex import exceptions -from PyPowerFlex.objects.gen2.protection_domain import ProtectionDomain from tests.common import PyPowerFlexTestCase @@ -145,7 +144,7 @@ def test_protection_domain_delete_bad_status(self): self.assertRaises(exceptions.PowerFlexFailDeleting, self.client.protection_domain.delete, self.fake_pd_id) - + def test_protection_domain_activate(self): """ Test the activation of a protection domain. @@ -180,7 +179,8 @@ def test_protection_domain_rebuild(self): """ Test the rebuild of a protection domain. """ - self.client.protection_domain.set_rebuild_enabled(self.fake_pd_id, False) + self.client.protection_domain.set_rebuild_enabled( + self.fake_pd_id, False) def test_protection_domain_rebuild_bad_status(self): """ @@ -190,12 +190,13 @@ def test_protection_domain_rebuild_bad_status(self): self.assertRaises(exceptions.PowerFlexClientException, self.client.protection_domain.set_rebuild_enabled, self.fake_pd_id, False) - + def test_protection_domain_rebalance(self): """ Test the rebalance of a protection domain. """ - self.client.protection_domain.set_rebalance_enabled(self.fake_pd_id, False) + self.client.protection_domain.set_rebalance_enabled( + self.fake_pd_id, False) def test_protection_domain_rebalance_bad_status(self): """ @@ -205,7 +206,7 @@ def test_protection_domain_rebalance_bad_status(self): self.assertRaises(exceptions.PowerFlexClientException, self.client.protection_domain.set_rebalance_enabled, self.fake_pd_id, False) - + def test_protection_domain_set_secondary_io_policy(self): """ Test the set_secondary_io_policy of a protection domain. diff --git a/tests/gen2/test_storage_pool.py b/tests/gen2/test_storage_pool.py index aad22a5..920c9d3 100644 --- a/tests/gen2/test_storage_pool.py +++ b/tests/gen2/test_storage_pool.py @@ -18,7 +18,6 @@ # pylint: disable=invalid-name,too-many-public-methods from PyPowerFlex import exceptions -from PyPowerFlex.objects.gen2.storage_pool import StoragePool from tests.common import PyPowerFlexTestCase @@ -28,6 +27,7 @@ class TestStoragePoolClient(PyPowerFlexTestCase): Tests for the StoragePoolClient class. """ + # pylint: disable=R0801 def setUp(self): """ Set up the test environment. @@ -94,7 +94,8 @@ def test_storage_pool_get_by_name(self): """ Test the get_by_name of a storage pool. """ - self.client.storage_pool.get_by_name(self.fake_pd_id, self.fake_sp_name) + self.client.storage_pool.get_by_name( + self.fake_pd_id, self.fake_sp_name) def test_storage_pool_update(self): """ @@ -134,8 +135,8 @@ def test_storage_pool_update_bad_status(self): 'zeroPaddingEnabled': True, } self.assertRaises(exceptions.PowerFlexClientException, - self.client.storage_pool.update, - sp) + self.client.storage_pool.update, + sp) def test_storage_pool_update_bad_status_1(self): """ @@ -158,8 +159,8 @@ def test_storage_pool_update_bad_status_1(self): 'zeroPaddingEnabled': True, } self.assertRaises(exceptions.PowerFlexClientException, - self.client.storage_pool.update, - sp) + self.client.storage_pool.update, + sp) def test_storage_pool_update_bad_status_2(self): """ @@ -182,8 +183,8 @@ def test_storage_pool_update_bad_status_2(self): 'zeroPaddingEnabled': True, } self.assertRaises(exceptions.PowerFlexClientException, - self.client.storage_pool.update, - sp) + self.client.storage_pool.update, + sp) def test_storage_pool_create(self): """ @@ -232,6 +233,7 @@ def test_storage_pool_create_bad_status(self): self.client.storage_pool.create, sp) + # pylint: disable=R0801 def test_storage_pool_delete(self): """ Test the deletion of a storage pool. @@ -246,12 +248,13 @@ def test_storage_pool_delete_bad_status(self): self.assertRaises(exceptions.PowerFlexFailDeleting, self.client.storage_pool.delete, self.fake_sp_id) - + def test_storage_pool_set_capacity_alert_thresholds(self): """ Test the set_capacity_alert_thresholds of a storage pool. """ - self.client.storage_pool.set_capacity_alert_thresholds(self.fake_sp_id, 1, 2) + self.client.storage_pool.set_capacity_alert_thresholds( + self.fake_sp_id, 1, 2) def test_storage_pool_set_capacity_alert_thresholds_bad_status(self): """ @@ -266,7 +269,8 @@ def test_storage_pool_set_over_provisioning_factor(self): """ Test the set_over_provisioning_factor of a storage pool. """ - self.client.storage_pool.set_over_provisioning_factor(self.fake_sp_id, 0) + self.client.storage_pool.set_over_provisioning_factor( + self.fake_sp_id, 0) def test_storage_pool_set_over_provisioning_factor_bad_status(self): """ @@ -291,12 +295,13 @@ def test_storage_pool_resize_bad_status(self): self.assertRaises(exceptions.PowerFlexClientException, self.client.storage_pool.resize, self.fake_sp_id, 1) - + def test_storage_pool_set_compression_method(self): """ Test the set_compression_method of a storage pool. """ - self.client.storage_pool.set_compression_method(self.fake_sp_id, "None") + self.client.storage_pool.set_compression_method( + self.fake_sp_id, "None") def test_storage_pool_set_compression_method_bad_status(self): """ @@ -306,12 +311,13 @@ def test_storage_pool_set_compression_method_bad_status(self): self.assertRaises(exceptions.PowerFlexClientException, self.client.storage_pool.set_compression_method, self.fake_sp_id, "None") - + def test_storage_pool_set_zero_padding_policy(self): """ Test the set_zero_padding_policy of a storage pool. """ - self.client.storage_pool.set_zero_padding_policy(self.fake_sp_id, False) + self.client.storage_pool.set_zero_padding_policy( + self.fake_sp_id, False) def test_storage_pool_set_zero_padding_policy_bad_status(self): """ @@ -322,6 +328,7 @@ def test_storage_pool_set_zero_padding_policy_bad_status(self): self.client.storage_pool.set_zero_padding_policy, self.fake_sp_id, False) + # pylint: disable=R0801 def test_storage_pool_rename(self): """ Test the rename method of a storage pool. From fcc752e2ca107d67e5186f3e78e771da4fc1e96f Mon Sep 17 00:00:00 2001 From: Yiming Bao Date: Thu, 31 Jul 2025 16:27:10 +0800 Subject: [PATCH 11/15] move test_base.py to common folder --- tests/{gen1 => common}/test_base.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{gen1 => common}/test_base.py (100%) diff --git a/tests/gen1/test_base.py b/tests/common/test_base.py similarity index 100% rename from tests/gen1/test_base.py rename to tests/common/test_base.py From 821743b906fffc05509116ea00dc6efc0506f4b7 Mon Sep 17 00:00:00 2001 From: Yiming Bao Date: Fri, 1 Aug 2025 11:13:55 +0800 Subject: [PATCH 12/15] move query_metrics to base class --- PyPowerFlex/base_client.py | 29 ++++++++++++++++++++++++++ PyPowerFlex/objects/common/utility.py | 30 --------------------------- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/PyPowerFlex/base_client.py b/PyPowerFlex/base_client.py index d0575a9..5723413 100644 --- a/PyPowerFlex/base_client.py +++ b/PyPowerFlex/base_client.py @@ -621,6 +621,35 @@ def _query_selected_statistics(self, action, params=None): raise exc return response + def query_metrics(self, resource_type, ids=None, metrics=None): + """Query PowerFlex resource metrics. + + :param resource_type: str + :param ids: list + :param metrics: list + :return: dict + """ + + params = { + 'resource_type': resource_type + } + if ids is not None: + params['ids'] = ids + if metrics is not None: + params['metrics'] = metrics + + r, response = self.send_post_request(self.metrics_query_url, + use_base_url=False, + params=params) + if r.status_code != requests.codes.ok: + msg = ( + f"Failed to query {resource_type} statistics. " + f"Error: {response}" + ) + LOG.error(msg) + raise exceptions.PowerFlexClientException(msg) + + return response class BaseSchema(Schema): """Base schema.""" diff --git a/PyPowerFlex/objects/common/utility.py b/PyPowerFlex/objects/common/utility.py index 5bd1991..28d1cc2 100644 --- a/PyPowerFlex/objects/common/utility.py +++ b/PyPowerFlex/objects/common/utility.py @@ -155,33 +155,3 @@ def get_statistics_for_all_storage_nodes(self, ids=None, metrics=None): """ metrics = metrics or StorageNodeConstants.DEFAULT_STATISTICS_METRICS return self.query_metrics('storage_node', ids, metrics) - - def query_metrics(self, resource_type, ids=None, metrics=None): - """Query PowerFlex resource metrics. - - :param resource_type: str - :param ids: list - :param metrics: list - :return: dict - """ - - params = { - 'resource_type': resource_type - } - if ids is not None: - params['ids'] = ids - if metrics is not None: - params['metrics'] = metrics - - r, response = self.send_post_request(self.metrics_query_url, - use_base_url=False, - params=params) - if r.status_code != requests.codes.ok: - msg = ( - f"Failed to query {resource_type} statistics. " - f"Error: {response}" - ) - LOG.error(msg) - raise exceptions.PowerFlexClientException(msg) - - return response From 68debd2fca006c21d6478fbf48f54201b11e6c66 Mon Sep 17 00:00:00 2001 From: Yiming Bao Date: Mon, 4 Aug 2025 13:59:57 +0800 Subject: [PATCH 13/15] add __init__ to tests --- tests/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 From 89cb2a5020e09a1be0336c1451575bc58f7a2cd2 Mon Sep 17 00:00:00 2001 From: Yiming Bao Date: Mon, 4 Aug 2025 14:13:33 +0800 Subject: [PATCH 14/15] remove unused code --- PyPowerFlex/objects/gen2/protection_domain.py | 38 -------- PyPowerFlex/objects/gen2/storage_pool.py | 94 ++----------------- 2 files changed, 9 insertions(+), 123 deletions(-) diff --git a/PyPowerFlex/objects/gen2/protection_domain.py b/PyPowerFlex/objects/gen2/protection_domain.py index 8d0ced6..d93f797 100644 --- a/PyPowerFlex/objects/gen2/protection_domain.py +++ b/PyPowerFlex/objects/gen2/protection_domain.py @@ -27,19 +27,6 @@ LOG = logging.getLogger(__name__) -# class LinkSchema(base_client.BaseSchema): -# rel = fields.Str( -# metadata={ -# "description": "Rel", -# } -# ) -# href = fields.Str( -# metadata={ -# "description": "Href", -# } -# ) - - class ProtectionDomainSchema(base_client.BaseSchema): """Protection Domain schema.""" id = fields.Str( @@ -143,11 +130,6 @@ class ProtectionDomainSchema(base_client.BaseSchema): "updatable": True, } ) - # links = fields.List(fields.Nested(LinkSchema), - # metadata={ - # "description": "Links", - # } - # ) def load_protection_domain_schema(obj): @@ -501,23 +483,3 @@ def rename(self, protection_domain_id, name): params = {"name": name} self._rename_entity(action, protection_domain_id, params) - - # def query_selected_statistics(self, properties, ids=None): - # """Query PowerFlex protection domain statistics. - - # :type properties: list - # :type ids: list of protection domain IDs or None for all protection - # domains - # :rtype: dict - # """ - - # action = "querySelectedStatistics" - - # params = {'properties': properties} - - # if ids: - # params["ids"] = ids - # else: - # params["allIds"] = "" - - # return self._query_selected_statistics(action, params) diff --git a/PyPowerFlex/objects/gen2/storage_pool.py b/PyPowerFlex/objects/gen2/storage_pool.py index d796ebe..ecc4580 100644 --- a/PyPowerFlex/objects/gen2/storage_pool.py +++ b/PyPowerFlex/objects/gen2/storage_pool.py @@ -150,6 +150,7 @@ class StoragePoolSchema(base_client.BaseSchema): # class Meta: # unknown = INCLUDE + def load_storage_pool_schema(obj): """Load storage pool schema.""" return StoragePoolSchema().load(obj) @@ -184,7 +185,8 @@ def get_by_name(self, protion_domain_id, name): """ pdo = ProtectionDomain(self.token, self.configuration) - result = pdo.get_storage_pools(protion_domain_id, filter_fields={"name": name}) + result = pdo.get_storage_pools( + protion_domain_id, filter_fields={"name": name}) if len(result) >= 1: return load_storage_pool_schema(result[0]) return None @@ -231,8 +233,10 @@ def update(self, sp, current_sp=None): :type sp: dict :rtype: dict """ - current_sp = current_sp if current_sp is not None else self.get_by_id(sp["id"]) - sp = load_storage_pool_schema({**StoragePoolSchema().dump(current_sp), **sp}) + current_sp = current_sp if current_sp is not None else self.get_by_id( + sp["id"]) + sp = load_storage_pool_schema( + {**StoragePoolSchema().dump(current_sp), **sp}) if sp["protection_domain_id"] != current_sp["protection_domain_id"]: e = exceptions.nonupdatable_exception( @@ -282,7 +286,8 @@ def update(self, sp, current_sp=None): if sp["over_provisioning_factor"] != current_sp["over_provisioning_factor"]: has_update = True - self.set_over_provisioning_factor(sp["id"], sp["over_provisioning_factor"]) + self.set_over_provisioning_factor( + sp["id"], sp["over_provisioning_factor"]) if sp["physical_size_gb"] != current_sp["physical_size_gb"]: has_update = True @@ -303,68 +308,6 @@ def delete(self, storage_pool_id): return self._delete_entity(storage_pool_id) - # def get_devices(self, storage_pool_id, filter_fields=None, fields=None): - # """Get related PowerFlex devices for storage pool. - - # :type storage_pool_id: str - # :type filter_fields: dict - # :type fields: list|tuple - # :rtype: list[dict] - # """ - - # return self.get_related(storage_pool_id, - # 'Device', - # filter_fields, - # fields) - - # def get_sdss(self, storage_pool_id, filter_fields=None, fields=None): - # """Get related PowerFlex SDSs for storage pool. - - # :type storage_pool_id: str - # :type filter_fields: dict - # :type fields: list|tuple - # :rtype: list[dict] - # """ - - # sdss_ids = self.get_related(storage_pool_id, - # 'SpSds', - # filter_fields, - # fields=('sdsId',)) - # sds_id_list = [sds['sdsId'] for sds in sdss_ids] - # if filter_fields: - # filter_fields.update({'id': sds_id_list}) - # filter_fields.pop('sdsId', None) - # else: - # filter_fields = {'id': sds_id_list} - # return Sds(self.token, self.configuration).get( - # filter_fields=filter_fields, fields=fields) - - # def get_volumes(self, storage_pool_id, filter_fields=None, fields=None): - # """Get related PowerFlex volumes for storage pool. - - # :type storage_pool_id: str - # :type filter_fields: dict - # :type fields: list|tuple - # :rtype: list[dict] - # """ - - # return self.get_related(storage_pool_id, - # 'Volume', - # filter_fields, - # fields) - - # def get_statistics(self, storage_pool_id, fields=None): - # """Get related PowerFlex Statistics for storage pool. - - # :type storage_pool_id: str - # :type fields: list|tuple - # :rtype: dict - # """ - - # return self.get_related(storage_pool_id, - # 'Statistics', - # fields) - def rename(self, storage_pool_id, name): """Rename PowerFlex storage pool. @@ -520,22 +463,3 @@ def set_zero_padding_policy(self, storage_pool_id, zero_padding_enabled): ) LOG.error(msg) raise exceptions.PowerFlexClientException(msg) - - # def query_selected_statistics(self, properties, ids=None): - # """Query PowerFlex storage pool statistics. - - # :type properties: list - # :type ids: list of storage pools IDs or None for all storage pools - # :rtype: dict - # """ - - # action = "querySelectedStatistics" - - # params = {'properties': properties} - - # if ids: - # params["ids"] = ids - # else: - # params["allIds"] = "" - - # return self._query_selected_statistics(action, params) From 8de7d4be4ce6479624cc77d2192f21e4c1baaf1d Mon Sep 17 00:00:00 2001 From: Yiming Bao Date: Mon, 4 Aug 2025 14:26:00 +0800 Subject: [PATCH 15/15] rename get_statistics_for_all_storage_nodes --- PyPowerFlex/objects/common/utility.py | 2 +- tests/common/test_utility.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/PyPowerFlex/objects/common/utility.py b/PyPowerFlex/objects/common/utility.py index 28d1cc2..0ea1c12 100644 --- a/PyPowerFlex/objects/common/utility.py +++ b/PyPowerFlex/objects/common/utility.py @@ -146,7 +146,7 @@ def get_statistics_for_all_snapshot_policies( return response - def get_statistics_for_all_storage_nodes(self, ids=None, metrics=None): + def query_metrics_for_all_storage_nodes(self, ids=None, metrics=None): """list storage node statistics for PowerFlex 5.0+. :param ids: list diff --git a/tests/common/test_utility.py b/tests/common/test_utility.py index 7bf5701..9626f4d 100644 --- a/tests/common/test_utility.py +++ b/tests/common/test_utility.py @@ -72,8 +72,8 @@ def test_get_statistics_for_all_volumes_bad_status(self): self.assertRaises(exceptions.PowerFlexClientException, self.client.utility.get_statistics_for_all_volumes) - def test_get_statistics_for_all_storage_nodes(self): + def test_query_metrics_for_all_storage_nodes(self): """ - Test the get_statistics_for_all_storage_nodes method. + Test the query_metrics_for_all_storage_nodes method. """ - self.client.utility.get_statistics_for_all_storage_nodes() + self.client.utility.query_metrics_for_all_storage_nodes()