Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions PyPowerFlex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

# pylint: disable=invalid-name,too-many-arguments,too-many-positional-arguments

import logging

from packaging import version

from PyPowerFlex import configuration
Expand All @@ -27,6 +29,8 @@
from PyPowerFlex.objects import gen1
from PyPowerFlex.objects import gen2

LOG = logging.getLogger(__name__)

__all__ = [
'PowerFlexClient'
]
Expand Down Expand Up @@ -114,13 +118,40 @@ def initialize(self):
'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'):
if self.__is_gen2():
self.add_objects_gen2()
else:
self.add_objects_gen1()
self.__is_initialized = True

def __is_gen2(self):
"""Check whether the system must be handled as a Gen2 system.

The API version alone is not sufficient: the REST API and the
PowerFlex components are upgraded independently, so a Gen1 system
running components 4.5.x can already expose API version 5.1. Such a
system must still be driven with the Gen1 objects.

An API version below 5.0 is only served by Gen1 systems, so it is
conclusive on its own. From API 5.0 onwards the component version
decides, since that is the version of PowerFlex itself.

:rtype: bool
"""

api_version = self.system.api_version()
if version.parse(api_version) < version.Version('5.0'):
return False

component_version = self.system.component_version()
is_gen2 = version.parse(component_version) >= version.Version('5.0')
if not is_gen2:
LOG.info(
"PowerFlex API version %s is served by component version %s, "
"handling the system as Gen1.", api_version, component_version
)
return is_gen2

def add_objects_common(self):
"""Add common objects here."""
self.__add_storage_entity('system', common.System)
Expand Down
65 changes: 65 additions & 0 deletions PyPowerFlex/objects/common/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class System(base_client.EntityRequest):
def __init__(self, token, configuration):
self.__api_version = None
self.__pfmp_version = None
self.__component_version = None
super().__init__(token, configuration)

def api_version(self, cached=True):
Expand Down Expand Up @@ -105,6 +106,70 @@ def pfmp_version(self, cached=True):
self.__pfmp_version = response.get('clusterVersion')
return self.__pfmp_version

def component_version(self, cached=True):
"""Get the PowerFlex component (Core/MDM) version.

The component version is the version of the PowerFlex software itself,
which may differ from the REST API version reported by
:meth:`api_version`. For example, a system can expose API version 5.1
while still running component version 4.5.x, because the API and the
components are upgraded independently.

The version is read from the MDM cluster master (``versionInfo``) and
falls back to ``systemVersionName``. Values such as ``R4_5.6000.162``
or ``DellEMC PowerFlex Version: R4_5.6000.162`` are normalised to
``4.5.6000.162``.

If the component version cannot be determined, the API version is
returned so that callers keep the previous API-version based
behaviour instead of guessing a generation.

:param cached: get component version from cache or send API response
:type cached: bool
:rtype: str
"""

if self.__component_version and cached:
return self.__component_version

raw_version = None
try:
system_info = self.get()
if system_info:
system = system_info[0]
raw_version = (
system.get('mdmCluster', {}).get('master', {}).get('versionInfo')
or system.get('systemVersionName')
)
except Exception as e:
LOG.debug("Failed to query the component version: %s", e)

self.__component_version = self.__normalize_version(raw_version) \
or self.api_version()
return self.__component_version

@staticmethod
def __normalize_version(raw_version):
"""Normalise a PowerFlex version string to a comparable version.

e.g. ``DellEMC PowerFlex Version: R4_5.6000.162`` -> ``4.5.6000.162``

:param raw_version: version string reported by the system
:type raw_version: str
:rtype: str
"""

if not raw_version:
return None
match = re.search(r'R\s*(\d+)[._](\d+)([\d.]*)', raw_version)
if not match:
LOG.warning(
"Could not determine the component version from '%s'.",
raw_version
)
return None
return f"{match.group(1)}.{match.group(2)}{match.group(3)}"

def remove_cg_snapshots(self, system_id, cg_id, allow_ext_managed=None):
"""Remove PowerFlex ConsistencyGroup snapshots.

Expand Down
10 changes: 6 additions & 4 deletions PyPowerFlex/objects/gen1/replication_consistency_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,18 @@ def resume(self, rcg_id):

return self._perform_entity_operation_based_on_action(rcg_id, "resume")

def failover(self, rcg_id):
def failover(self, rcg_id, force=False):
"""Failover PowerFlex RCG.

:param rcg_id: str
:param force: bool
:return: dict
"""

url_params = {
'force': force
}
return self._perform_entity_operation_based_on_action(
rcg_id, "failover")
rcg_id, "failover", **url_params)

def sync(self, rcg_id):
"""Synchronize PowerFlex RCG.
Expand Down Expand Up @@ -213,7 +216,6 @@ def reverse(self, rcg_id):
:param rcg_id: str
:return: dict
"""

return self._perform_entity_operation_based_on_action(
rcg_id, "reverse")

Expand Down
34 changes: 33 additions & 1 deletion tests/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,39 @@ class PyPowerFlexTestCase(TestCase):
Provides a mocked HTTP response for testing.
"""
VERSION_API_PATH = '/version'
SYSTEM_API_PATH = '/types/System/instances'

@staticmethod
def system_instances(component_version):
"""
Build a System instances response reporting the given component version.

Args:
component_version (str): Component version, e.g. '4.5'.

Returns:
list: The mocked System instances response.
"""
version_info = f"R{component_version.replace('.', '_', 1)}.0.0"
return [
{
'id': '1',
'mdmCluster': {'master': {'versionInfo': version_info}},
'systemVersionName':
f'DellEMC PowerFlex Version: {version_info}',
}
]

@classmethod
def version(cls, new_version):
def version(cls, new_version, component_version=None):
"""
Decorator for mocking the version API version.

Args:
new_version (str): The REST API version of the system.
component_version (str): Optional component (Core/MDM) version,
when it differs from the API version. Defaults to the API
version, i.e. a system whose API and components are in sync.
"""

def decorator(subclass):
Expand All @@ -96,6 +124,10 @@ def decorator(subclass):
subclass.DEFAULT_MOCK_RESPONSES[
cls.RESPONSE_MODE.Valid
][cls.VERSION_API_PATH] = new_version
subclass.DEFAULT_MOCK_RESPONSES[
cls.RESPONSE_MODE.Valid
][cls.SYSTEM_API_PATH] = cls.system_instances(
component_version or new_version)
return subclass

return decorator
Expand Down
Loading