diff --git a/cterasdk/common/utils.py b/cterasdk/common/utils.py index bf0e82e6..37787e00 100644 --- a/cterasdk/common/utils.py +++ b/cterasdk/common/utils.py @@ -3,7 +3,7 @@ import logging from datetime import datetime -from packaging.version import parse as parse_version +from packaging.version import InvalidVersion, parse as parse_version from .object import Object from .enum import DayOfWeek @@ -157,7 +157,11 @@ class Version: """Software Version""" def __init__(self, version): - self._version = parse_version(version) + try: + self._version = parse_version(version) + except (InvalidVersion, TypeError, ValueError): + logger.warning('Unrecognized software version %r, using 0.0.0', version) + self._version = parse_version('0.0.0') def __eq__(self, v): return self._version == parse_version(v) diff --git a/cterasdk/core/__init__.py b/cterasdk/core/__init__.py index f074c691..a8149400 100644 --- a/cterasdk/core/__init__.py +++ b/cterasdk/core/__init__.py @@ -9,6 +9,7 @@ 'directoryservice', 'enum', 'files', + 'fusion_direct', 'login', 'logs', 'messaging', diff --git a/cterasdk/core/cloudfs.py b/cterasdk/core/cloudfs.py index 96d03fb7..a8cc18d2 100644 --- a/cterasdk/core/cloudfs.py +++ b/cterasdk/core/cloudfs.py @@ -4,7 +4,7 @@ from .base_command import BaseCommand from . import query, devices -from .enum import ListFilter, PolicyType +from .enum import Duration, ListFilter, PolicyType from .types import ComplianceSettingsBuilder, ExtendedAttributesBuilder from ..common import union, Object from ..exceptions import CTERAException, ObjectNotFoundException @@ -12,6 +12,39 @@ logger = logging.getLogger('cterasdk.core') +# Suggested ``include`` paths when listing or fetching Fusion Direct cloud folders. +# Portal response fields keep legacy names ``openStorageEnabled``, ``openFabricSettings``, etc. +CLOUD_DRIVE_FUSION_DIRECT_INCLUDE = [ + 'openStorageEnabled', + 'openFabricSettings', + 'openFabricStorageStatus', + 'openFabricStorageStatusDetails', +] + + +def _worm_period(amount, duration_type): + period = Object() + period._classname = 'WormPeriod' # pylint: disable=protected-access + period.amount = amount + period.type = duration_type + return period + + +def _default_archive_settings(): + """Defaults aligned with portal ``CloudDriveCreateParams`` / Java ``AdminApiOperations.createCloudFolderParams``. + + Used when :meth:`CloudDrives.add` / :meth:`CloudDrives.add_return_id` are called without ``archive_settings``. + """ + arch = Object() + arch._classname = 'ArchiveSettings' # pylint: disable=protected-access + arch.archive = False + arch.gracePeriod = _worm_period(0, Duration.Minutes) + arch.retentionMode = 'Enterprise' + arch.retentionPeriod = _worm_period(30, Duration.Days) + arch.deleteData = False + arch.seal = False + return arch + class CloudFS(BaseCommand): """ @@ -149,29 +182,25 @@ def __init__(self, core): self.locks = Locks(self._core) def _get_entire_object(self, name, owner): - return self._core.api.get(f'{self.find(name, owner, include=["baseObjectRef"]).baseObjectRef}') + # Fusion Direct fields (``openFabricSettings`` / ``openStorageEnabled``) for read-modify-write on PUT. + include = union(CLOUD_DRIVE_FUSION_DIRECT_INCLUDE, ['baseObjectRef']) + ref = self.find(name, owner, include=include).baseObjectRef + return self._core.api.get(ref) - def add(self, name, group, owner, winacls=True, description=None, # pylint: disable=too-many-arguments - quota=None, compliance_settings=None, xattrs=None, gfl=False, lock_extensions=None): - """ - Create a new Cloud Drive Folder (Cloud Volume) + @staticmethod + def _parse_add_cloud_drive_response(response): + if isinstance(response, str): + match = re.search(r'/Users/(.+)', response) + if match: + return match.group() + return response + return response - :param str name: Name of the new folder - :param str group: Folder Group to assign this folder to - :param cterasdk.core.types.UserAccount owner: User account, the owner of the new folder - :param bool,optional winacls: Use Windows ACLs, defaults to True - :param str,optional description: Cloud drive folder description - :param str,optional quota: Cloud drive folder quota in GB - :param cterasdk.common.object.Object,optional compliance_settings: Compliance settings, defaults to disabled. - Use :func:`cterasdk.core.types.ComplianceSettingsBuilder` to build the compliance settings object - :param cterasdk.common.object.Object,optional xattrs: Extended attributes, defaults to MacOS. - Use :func:`cterasdk.core.types.ExtendedAttributesBuilder` to build the extended attributes object - :param bool,optional gfl: Enable global file locking - :param list[str],optional lock_extensions: List of file extensions (without leading dot) for which global file locking is enforced. - :returns: Path to the Cloud Drive folder - :rtype: str - """ + def _build_cloud_drive_create_params(self, name, group, owner, winacls=True, description=None, # pylint: disable=too-many-arguments + quota=None, compliance_settings=None, xattrs=None, gfl=False, lock_extensions=None, + archive_settings=None): param = Object() + param._classname = 'CloudDriveCreateParams' # pylint: disable=protected-access param.name = name param.owner = self._core.users.get(owner, ['baseObjectRef']).baseObjectRef param.group = self._core.cloudfs.groups.get(group, ['baseObjectRef']).baseObjectRef @@ -193,6 +222,7 @@ def add(self, name, group, owner, winacls=True, description=None, # pylint: dis else: param.extendedAttributes = ExtendedAttributesBuilder.default().build() + param.archiveSettings = archive_settings if archive_settings is not None else _default_archive_settings() if gfl: param.globalFileLockSettings = Object() param.globalFileLockSettings._classname = 'GlobalFileLockSettings' # pylint: disable=protected-access @@ -200,6 +230,73 @@ def add(self, name, group, owner, winacls=True, description=None, # pylint: dis param.globalFileLockSettings.globalFileLockExtensions = ( lock_extensions if lock_extensions else CloudDrives.default_extensions ) + else: + param.globalFileLockSettings = Object() + param.globalFileLockSettings._classname = 'GlobalFileLockSettings' # pylint: disable=protected-access + param.globalFileLockSettings.enabled = False + param.globalFileLockSettings.globalFileLockExtensions = ( + lock_extensions if lock_extensions else CloudDrives.default_extensions + ) + + return param + + @staticmethod + def _apply_fusion_direct_add_params(param, open_fabric_settings, open_storage_enabled): + if open_fabric_settings is not None: + if open_storage_enabled is False: + raise CTERAException( + 'openStorageEnabled cannot be False when openFabricSettings is set.' + ) + param.openFabricSettings = open_fabric_settings + param.openStorageEnabled = True if open_storage_enabled is None else bool(open_storage_enabled) + elif open_storage_enabled is not None: + param.openStorageEnabled = open_storage_enabled + + @staticmethod + def _apply_fusion_direct_modify_params(param, open_fabric_settings, open_storage_enabled): + if open_fabric_settings is not None: + if open_storage_enabled is False: + raise CTERAException( + 'openStorageEnabled cannot be False when openFabricSettings is set.' + ) + param.openFabricSettings = open_fabric_settings + if open_storage_enabled is not None: + param.openStorageEnabled = bool(open_storage_enabled) + elif open_storage_enabled is not None: + param.openStorageEnabled = open_storage_enabled + + def add(self, name, group, owner, winacls=True, description=None, # pylint: disable=too-many-arguments, too-many-locals + quota=None, compliance_settings=None, xattrs=None, gfl=False, lock_extensions=None, + open_fabric_settings=None, open_storage_enabled=None, archive_settings=None): + """ + Create a new Cloud Drive Folder (Cloud Volume) + + :param str name: Name of the new folder + :param str group: Folder Group to assign this folder to + :param cterasdk.core.types.UserAccount owner: User account, the owner of the new folder + :param bool,optional winacls: Use Windows ACLs, defaults to True + :param str,optional description: Cloud drive folder description + :param str,optional quota: Cloud drive folder quota in GB + :param cterasdk.common.object.Object,optional compliance_settings: Compliance settings, defaults to disabled. + Use :func:`cterasdk.core.types.ComplianceSettingsBuilder` to build the compliance settings object + :param cterasdk.common.object.Object,optional xattrs: Extended attributes, defaults to MacOS. + Use :func:`cterasdk.core.types.ExtendedAttributesBuilder` to build the extended attributes object + :param bool,optional gfl: Enable global file locking + :param list[str],optional lock_extensions: List of file extensions (without leading dot) for which global file locking is enforced. + :param cterasdk.common.object.Object,optional open_fabric_settings: Fusion Direct ``OpenFabricSettings`` object tree + (e.g. from :class:`cterasdk.core.fusion_direct.OpenFabricSettingsBuilder`). When set, ``openStorageEnabled`` defaults to ``True``; + passing ``open_storage_enabled=False`` raises :class:`cterasdk.exceptions.CTERAException`. + :param bool,optional open_storage_enabled: When ``open_fabric_settings`` is omitted, sets ``openStorageEnabled`` on the + create payload if not ``None``. Must not be ``False`` when ``open_fabric_settings`` is set. + :param cterasdk.common.object.Object,optional archive_settings: ``ArchiveSettings`` object tree. When omitted, a portal-aligned + default (archive off, enterprise retention) built by ``_default_archive_settings()`` is used. + :returns: WebDAV-style ``/Users/...`` path string when the portal returns that form; otherwise the raw execute response. + :rtype: str or object + """ + param = self._build_cloud_drive_create_params( + name, group, owner, winacls, description, quota, compliance_settings, xattrs, gfl, lock_extensions, + archive_settings=archive_settings) + self._apply_fusion_direct_add_params(param, open_fabric_settings, open_storage_enabled) try: response = self._core.api.execute('', 'addCloudDrive', param) @@ -207,7 +304,42 @@ def add(self, name, group, owner, winacls=True, description=None, # pylint: dis 'Cloud drive folder created. %s', {'name': name, 'owner': param.owner, 'folder_group': group, 'winacls': winacls} ) - return re.search(r'/Users\/(.+)', response).group() + return CloudDrives._parse_add_cloud_drive_response(response) + except CTERAException as error: + logger.error( + 'Cloud drive folder creation failed. %s', + {'name': name, 'folder_group': group, 'owner': str(owner), 'win_acls': winacls} + ) + raise error + + def add_return_id(self, name, group, owner, winacls=True, description=None, # pylint: disable=too-many-arguments, too-many-locals + quota=None, compliance_settings=None, xattrs=None, gfl=False, lock_extensions=None, + open_fabric_settings=None, open_storage_enabled=None, archive_settings=None): + """ + Create a Cloud Drive Folder using ``addCloudDriveReturnID`` (structured return codes and folder id). + + Parameters match :meth:`add`. Inspect ``rc`` and ``folderEntry`` on the returned object for success or errors. + + .. note:: + On some portal builds, a **global admin** session creating a folder for **another** user can leave the + folder created in the database yet return HTTP 500, because the ReturnID response path resolves the new + folder under the caller's principal instead of the owner. Prefer :meth:`add` in that scenario, or verify + the folder in the UI / listing before retrying with the same name. + + :returns: Parsed portal response (typically XML-backed object with ``rc``, ``folderEntry``, ``errorMsg``, etc.) + """ + param = self._build_cloud_drive_create_params( + name, group, owner, winacls, description, quota, compliance_settings, xattrs, gfl, lock_extensions, + archive_settings=archive_settings) + self._apply_fusion_direct_add_params(param, open_fabric_settings, open_storage_enabled) + + try: + response = self._core.api.execute('', 'addCloudDriveReturnID', param) + logger.info( + 'Cloud drive folder create (return id). %s', + {'name': name, 'owner': param.owner, 'folder_group': group, 'winacls': winacls} + ) + return response except CTERAException as error: logger.error( 'Cloud drive folder creation failed. %s', @@ -217,10 +349,13 @@ def add(self, name, group, owner, winacls=True, description=None, # pylint: dis def modify(self, current_name, owner, new_name=None, # pylint: disable=too-many-arguments, too-many-locals new_owner=None, new_group=None, description=None, winacls=None, quota=None, compliance_settings=None, xattrs=None, - gfl=None, lock_extensions=None): + gfl=None, lock_extensions=None, open_fabric_settings=None, open_storage_enabled=None): """ Modify a Cloud Drive Folder (Cloud Volume) + The folder is loaded with :data:`CLOUD_DRIVE_FUSION_DIRECT_INCLUDE` so + ``openFabricSettings`` / ``openStorageEnabled`` survive fields you do not change. + :param str current_name: Current folder name :param cterasdk.core.types.UserAccount owner: User account, the owner of the folder :param str,optional new_name: New folder name @@ -235,6 +370,13 @@ def modify(self, current_name, owner, new_name=None, # pylint: disable=too-many Use :func:`cterasdk.core.types.ExtendedAttributesBuilder` to build the extended attributes object :param bool,optional gfl: Enable global file locking :param list[str],optional lock_extensions: List of file extensions (without leading dot) for which global file locking is enforced. + :param cterasdk.common.object.Object,optional open_fabric_settings: Replacement Fusion Direct ``OpenFabricSettings`` object tree + (e.g. from :class:`cterasdk.core.fusion_direct.OpenFabricSettingsBuilder`). The portal forbids changing S3 bucket or + storage driver type after creation; ``storageMode`` and credentials may be updated within those rules. + To keep the existing secret when rebuilding ``dataStorage``, set ``secretKey`` to + :data:`cterasdk.core.fusion_direct.FUSION_DIRECT_SECRET_KEY_UNCHANGED`. + :param bool,optional open_storage_enabled: When ``open_fabric_settings`` is omitted, sets ``openStorageEnabled`` if not ``None``. + The portal rejects disabling Fusion Direct after creation; must not be ``False`` when ``open_fabric_settings`` is set. """ param = self._get_entire_object(current_name, owner) if new_name: @@ -260,6 +402,8 @@ def modify(self, current_name, owner, new_name=None, # pylint: disable=too-many lock_extensions if lock_extensions else CloudDrives.default_extensions ) + self._apply_fusion_direct_modify_params(param, open_fabric_settings, open_storage_enabled) + try: response = self._core.api.put(f'/{param.baseObjectRef}', param) logger.info('Cloud drive folder updated. %s', {'name': current_name}) diff --git a/cterasdk/core/enum.py b/cterasdk/core/enum.py index ed6809bd..4a9921cb 100644 --- a/cterasdk/core/enum.py +++ b/cterasdk/core/enum.py @@ -358,6 +358,17 @@ class ListFilter: NonDeleted = 'NonDeleted' +class OpenFabricStorageMode: + """ + Fusion Direct storage mode values for :class:`cterasdk.core.fusion_direct.OpenFabricSettingsBuilder`. + + Must match the portal schema enum name ``OpenFabricStorageMode`` (legacy identifier). + """ + Filesystem = 'Filesystem' + Bucket = 'Bucket' + Bidirectional = 'Bidirectional' + + class PlanCriteria: """ Subscription Plan Auto Assignment Rule Builder Criterias diff --git a/cterasdk/core/fusion_direct.py b/cterasdk/core/fusion_direct.py new file mode 100644 index 00000000..d63251e3 --- /dev/null +++ b/cterasdk/core/fusion_direct.py @@ -0,0 +1,94 @@ +""" +Fusion Direct cloud folder API payload builders. + +These produce :class:`cterasdk.common.object.Object` trees with ``_classname`` set for portal +``OpenFabricS3DataStorage`` and ``OpenFabricSettings`` schema types (legacy type names). +""" + +from .enum import OpenFabricStorageMode +from ..common import Object + + +# Default when ``storage`` is omitted; matches portal ``LocationsType.GenericS3`` name. +DEFAULT_FUSION_DIRECT_S3_STORAGE = 'GenericS3' + +# Matches portal ``com.ctera.infra.dataset.SimpleType.DONT_CHANGE_PASSWORD`` for ``dataStorage.secretKey`` +# on :meth:`cterasdk.core.cloudfs.CloudDrives.modify` when the secret should stay unchanged. +FUSION_DIRECT_SECRET_KEY_UNCHANGED = "*****DON'T CHANGE*****" + + +class OpenFabricS3DataStorageBuilder: # pylint: disable=too-many-instance-attributes + """ + Build ``OpenFabricS3DataStorage`` for ``OpenFabricSettings.dataStorage`` (Fusion Direct). + + The ``storage`` value must be a ``LocationsType`` enum name accepted by the portal + for Fusion Direct (for example ``GenericS3``, ``S3``, ``MinIOS3``). When omitted, + ``GenericS3`` is used, consistent with portal resolution of blank storage. + """ + + def __init__(self, bucket, access_key, secret_key, end_point, *, storage=None, use_https=True, # pylint: disable=too-many-arguments + region=None, trust_all_certificates=False, use_path_style_addressing=False, + sqs_url=None, metadata_tags=False): + self._bucket = bucket + self._access_key = access_key + self._secret_key = secret_key + self._end_point = end_point + self._storage = storage if storage is not None else DEFAULT_FUSION_DIRECT_S3_STORAGE + self._use_https = use_https + self._region = region + self._trust_all_certificates = trust_all_certificates + self._use_path_style_addressing = use_path_style_addressing + self._sqs_url = sqs_url + self._metadata_tags = metadata_tags + + def build(self): + """ + :returns: Object tree for ``OpenFabricS3DataStorage`` + :rtype: cterasdk.common.object.Object + """ + data = Object() + data._classname = 'OpenFabricS3DataStorage' # pylint: disable=protected-access + data.storage = self._storage + data.bucket = self._bucket + data.accessKey = self._access_key + data.secretKey = self._secret_key + data.endPoint = self._end_point + data.useHttps = self._use_https + if self._region is not None: + data.region = self._region + data.trustAllCertificates = self._trust_all_certificates + data.usePathStyleAddressing = self._use_path_style_addressing + if self._sqs_url is not None: + data.sqsUrl = self._sqs_url + data.metadataTags = self._metadata_tags + return data + + +class OpenFabricSettingsBuilder: + """ + Build ``OpenFabricSettings`` for :meth:`cterasdk.core.cloudfs.CloudDrives.add` + and :meth:`cterasdk.core.cloudfs.CloudDrives.modify` (Fusion Direct). + + For S3-backed Fusion Direct folders, use a non-``Filesystem`` ``storage_mode`` + (typically :attr:`OpenFabricStorageMode.Bucket`) and an ``OpenFabricS3DataStorage`` + instance from :class:`OpenFabricS3DataStorageBuilder`. + """ + + def __init__(self, data_storage, *, storage_mode=None): + """ + :param cterasdk.common.object.Object data_storage: Built ``OpenFabricS3DataStorage`` object + :param str,optional storage_mode: One of :class:`OpenFabricStorageMode`; defaults to ``Bucket`` + """ + self._data_storage = data_storage + self._storage_mode = storage_mode if storage_mode is not None else OpenFabricStorageMode.Bucket + + def build(self): + """ + :returns: Object tree for ``OpenFabricSettings`` + :rtype: cterasdk.common.object.Object + """ + settings = Object() + settings._classname = 'OpenFabricSettings' # pylint: disable=protected-access + settings.storageMode = self._storage_mode + settings.dataStorage = self._data_storage + return settings diff --git a/docs/source/UserGuides/Portal/Administration.rst b/docs/source/UserGuides/Portal/Administration.rst index df365eba..41a90d98 100644 --- a/docs/source/UserGuides/Portal/Administration.rst +++ b/docs/source/UserGuides/Portal/Administration.rst @@ -1414,6 +1414,34 @@ Cloud Drive Folders settings = core_types.ComplianceSettingsBuilder.enterprise(1, core_enum.Duration.Years).grace_period(1, core_enum.Duration.Hours).build() admin.cloudfs.drives.add('Compliance', 'FG-Compliance', svc_account, compliance_settings=settings) +.. automethod:: cterasdk.core.cloudfs.CloudDrives.add_return_id + :noindex: + +**Fusion Direct** + +Team Portal administrators with permission to manage cloud folders can create a **Fusion Direct** +cloud folder backed by a customer-managed S3-compatible bucket by passing ``open_fabric_settings``. +The bucket must have object **versioning** enabled, as enforced by the portal. + +Use :data:`cterasdk.core.cloudfs.CLOUD_DRIVE_FUSION_DIRECT_INCLUDE` with +:meth:`cterasdk.core.cloudfs.CloudDrives.all` or :meth:`cterasdk.core.cloudfs.CloudDrives.find` to retrieve +``openFabricSettings`` and related status fields. + +.. code:: python + + from cterasdk.core import fusion_direct + from cterasdk.core.cloudfs import CLOUD_DRIVE_FUSION_DIRECT_INCLUDE + + svc_account = core_types.UserAccount('svc_account') + s3_data = fusion_direct.OpenFabricS3DataStorageBuilder( + 'my-versioned-bucket', 'ACCESSKEY', 'SECRETKEY', 'https://s3.example.com', + ).build() + of_settings = fusion_direct.OpenFabricSettingsBuilder(s3_data).build() + admin.cloudfs.drives.add('OF-001', 'FG-001', svc_account, open_fabric_settings=of_settings) + result = admin.cloudfs.drives.add_return_id('OF-002', 'FG-001', svc_account, open_fabric_settings=of_settings) + for folder in admin.cloudfs.drives.all(include=CLOUD_DRIVE_FUSION_DIRECT_INCLUDE): + print(folder.name) + .. automethod:: cterasdk.core.cloudfs.CloudDrives.modify :noindex: @@ -1424,6 +1452,14 @@ Cloud Drive Folders svc_account = core_types.UserAccount('svc_account') admin.cloudfs.drives.modify('DIR-001', svc_account, quota=5120) # Set folder quota to 5 TB + """Rename or change Fusion Direct ``storageMode`` (within portal rules: same bucket/driver, empty folder for rename).""" + s3_data = fusion_direct.OpenFabricS3DataStorageBuilder( + 'my-versioned-bucket', 'ACCESSKEY', 'SECRETKEY', 'https://s3.example.com', + sqs_url='https://sqs...', + ).build() + of_settings = fusion_direct.OpenFabricSettingsBuilder(s3_data, storage_mode=core_enum.OpenFabricStorageMode.Bidirectional).build() + admin.cloudfs.drives.modify('OF-001', svc_account, new_name='OF-renamed', open_fabric_settings=of_settings) + .. automethod:: cterasdk.core.cloudfs.CloudDrives.delete :noindex: diff --git a/docs/source/api/cterasdk.core.fusion_direct.rst b/docs/source/api/cterasdk.core.fusion_direct.rst new file mode 100644 index 00000000..02c94353 --- /dev/null +++ b/docs/source/api/cterasdk.core.fusion_direct.rst @@ -0,0 +1,10 @@ +cterasdk.core.fusion_direct module +==================================== + +Fusion Direct payload builders. Schema class names (``OpenFabricSettings``, etc.) match portal types; +product name is **Fusion Direct**. + +.. automodule:: cterasdk.core.fusion_direct + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/cterasdk.core.rst b/docs/source/api/cterasdk.core.rst index ddba03c5..8bfc959d 100644 --- a/docs/source/api/cterasdk.core.rst +++ b/docs/source/api/cterasdk.core.rst @@ -34,6 +34,7 @@ Submodules cterasdk.core.logs cterasdk.core.mail cterasdk.core.messaging + cterasdk.core.fusion_direct cterasdk.core.portals cterasdk.core.query cterasdk.core.reports diff --git a/tests/ut/common/test_version.py b/tests/ut/common/test_version.py new file mode 100644 index 00000000..24f20545 --- /dev/null +++ b/tests/ut/common/test_version.py @@ -0,0 +1,14 @@ +import unittest + +from cterasdk.common.utils import Version + + +class TestVersionUnknown(unittest.TestCase): + + def test_unknown_version_falls_back(self): + v = Version('Unknown') + self.assertEqual(str(v), '0.0.0') + + def test_normal_version_unchanged(self): + v = Version('7.0.981.7') + self.assertIn('7', str(v)) diff --git a/tests/ut/core/admin/test_cloudfs_cloud_drives.py b/tests/ut/core/admin/test_cloudfs_cloud_drives.py index beeb58ab..30a61bdc 100644 --- a/tests/ut/core/admin/test_cloudfs_cloud_drives.py +++ b/tests/ut/core/admin/test_cloudfs_cloud_drives.py @@ -3,7 +3,10 @@ from cterasdk import exceptions from cterasdk.core import cloudfs +from cterasdk.core import fusion_direct +from cterasdk.core.cloudfs import _default_archive_settings from cterasdk.core.types import UserAccount, ComplianceSettingsBuilder, ExtendedAttributesBuilder +from cterasdk.core.enum import OpenFabricStorageMode from cterasdk.core import query from cterasdk.common import Object, union from tests.ut.core.admin import base_admin @@ -167,6 +170,79 @@ def test_add_cloud_drive_with_local_owner_raise(self): self.assertEqual(error_message, str(error.exception)) + def test_add_cloud_drive_with_explicit_archive_settings(self): + custom_arch = _default_archive_settings() + custom_arch.archive = True + self._init_global_admin(get_response='admin', execute_response=self._add_cloudfolder_response) + self._mock_get_user_base_object_ref() + self._mock_get_folder_group() + + cloudfs.CloudDrives(self._global_admin).add( + self._name, self._group, self._local_user_account, archive_settings=custom_arch) + + actual_param = self._global_admin.api.execute.call_args[0][2] + expected_param = self._get_add_cloud_drive_object(archive_settings=custom_arch) + self._assert_equal_objects(actual_param, expected_param) + + def test_add_cloud_drive_with_open_fabric_settings(self): + of_settings = fusion_direct.OpenFabricSettingsBuilder( + fusion_direct.OpenFabricS3DataStorageBuilder('b', 'k', 's', 'https://e').build(), + ).build() + self._init_global_admin(get_response='admin', execute_response=self._add_cloudfolder_response) + self._mock_get_user_base_object_ref() + self._mock_get_folder_group() + + ret = cloudfs.CloudDrives(self._global_admin).add( + self._name, self._group, self._local_user_account, open_fabric_settings=of_settings) + + self._global_admin.api.execute.assert_called_once_with('', 'addCloudDrive', mock.ANY) + expected_param = self._get_add_cloud_drive_object(open_fabric_settings=of_settings, open_storage_enabled=True) + actual_param = self._global_admin.api.execute.call_args[0][2] + self._assert_equal_objects(actual_param, expected_param) + self.assertEqual(ret, self._cloudfolder_path) + + def test_add_return_id_uses_add_cloud_drive_return_id(self): + of_settings = fusion_direct.OpenFabricSettingsBuilder( + fusion_direct.OpenFabricS3DataStorageBuilder('b', 'k', 's', 'https://e').build(), + ).build() + xml_response = Object() + xml_response.rc = '0' + self._init_global_admin(get_response='admin', execute_response=xml_response) + self._mock_get_user_base_object_ref() + self._mock_get_folder_group() + + ret = cloudfs.CloudDrives(self._global_admin).add_return_id( + self._name, self._group, self._local_user_account, open_fabric_settings=of_settings) + + self._global_admin.api.execute.assert_called_once_with('', 'addCloudDriveReturnID', mock.ANY) + expected_param = self._get_add_cloud_drive_object(open_fabric_settings=of_settings, open_storage_enabled=True) + actual_param = self._global_admin.api.execute.call_args[0][2] + self._assert_equal_objects(actual_param, expected_param) + self.assertIs(ret, xml_response) + + def test_add_open_fabric_with_open_storage_false_raises(self): + of_settings = fusion_direct.OpenFabricSettingsBuilder( + fusion_direct.OpenFabricS3DataStorageBuilder('b', 'k', 's', 'https://e').build(), + ).build() + self._init_global_admin(get_response='admin') + self._mock_get_user_base_object_ref() + self._mock_get_folder_group() + + with self.assertRaises(exceptions.CTERAException): + cloudfs.CloudDrives(self._global_admin).add( + self._name, self._group, self._local_user_account, + open_fabric_settings=of_settings, open_storage_enabled=False) + + self._global_admin.api.execute.assert_not_called() + + def test_add_cloud_drive_non_string_response_returned_unchanged(self): + self._init_global_admin(get_response='admin', execute_response=Object()) + self._mock_get_user_base_object_ref() + self._mock_get_folder_group() + + ret = cloudfs.CloudDrives(self._global_admin).add(self._name, self._group, self._local_user_account) + self.assertIsInstance(ret, Object) + def test_delete_with_local_owner(self): self._init_global_admin() with mock.patch("cterasdk.core.cloudfs.query.iterator") as query_iterator_mock: @@ -191,9 +267,12 @@ def test_undelete_with_local_owner(self): self._global_admin.users.get.assert_called_once_with(self._local_user_account, ['displayName']) self._global_admin.files.undelete.assert_called_once_with(f'Users/{self._owner}/{self._name}') + # pylint: disable-next=too-many-arguments def _get_add_cloud_drive_object(self, winacls=True, description=None, quota=None, compliance_settings=None, xattrs=None, - gfl=None, lock_extensions=None): + gfl=None, lock_extensions=None, open_fabric_settings=None, open_storage_enabled=None, + archive_settings=None): add_cloud_drive_param = Object() + add_cloud_drive_param._classname = 'CloudDriveCreateParams' # pylint: disable=protected-access add_cloud_drive_param.name = self._name add_cloud_drive_param.owner = self._owner add_cloud_drive_param.group = self._group @@ -203,6 +282,7 @@ def _get_add_cloud_drive_object(self, winacls=True, description=None, quota=None add_cloud_drive_param.description = description add_cloud_drive_param.wormSettings = compliance_settings if compliance_settings else ComplianceSettingsBuilder.default().build() add_cloud_drive_param.extendedAttributes = xattrs if xattrs else ExtendedAttributesBuilder.default().build() + add_cloud_drive_param.archiveSettings = archive_settings if archive_settings is not None else _default_archive_settings() if gfl: add_cloud_drive_param.globalFileLockSettings = Object() add_cloud_drive_param.globalFileLockSettings._classname = 'GlobalFileLockSettings' # pylint: disable=protected-access @@ -210,6 +290,17 @@ def _get_add_cloud_drive_object(self, winacls=True, description=None, quota=None add_cloud_drive_param.globalFileLockSettings.globalFileLockExtensions = ( lock_extensions if lock_extensions else cloudfs.CloudDrives.default_extensions ) + else: + add_cloud_drive_param.globalFileLockSettings = Object() + add_cloud_drive_param.globalFileLockSettings._classname = 'GlobalFileLockSettings' # pylint: disable=protected-access + add_cloud_drive_param.globalFileLockSettings.enabled = False + add_cloud_drive_param.globalFileLockSettings.globalFileLockExtensions = ( + lock_extensions if lock_extensions else cloudfs.CloudDrives.default_extensions + ) + if open_fabric_settings is not None: + add_cloud_drive_param.openFabricSettings = open_fabric_settings + if open_storage_enabled is not None: + add_cloud_drive_param.openStorageEnabled = open_storage_enabled return add_cloud_drive_param def _mock_get_user_base_object_ref(self): @@ -285,3 +376,35 @@ def test_modify_cloudfolder_failure(self): cloudfs.CloudDrives(self._global_admin).modify(self._cloudfolder_name, self._local_user_account) self._global_admin.api.get.assert_called_once_with(self._cloudfolder_baseObjecrRef) self._global_admin.api.put.assert_called_once_with(f'/{self._cloudfolder_baseObjecrRef}', mock.ANY) + include_used = mock_find_cloudfolder.call_args[1]['include'] + for field in cloudfs.CLOUD_DRIVE_FUSION_DIRECT_INCLUDE: + self.assertIn(field, include_used) + self.assertIn('baseObjectRef', include_used) + + def test_modify_cloud_drive_with_open_fabric_settings(self): + mock_find = self.patch_call('cterasdk.core.cloudfs.CloudDrives.find') + mock_find.return_value = munch.Munch({'baseObjectRef': self._cloudfolder_baseObjecrRef}) + existing = munch.Munch({'baseObjectRef': self._cloudfolder_baseObjecrRef, 'name': self._cloudfolder_name}) + new_of = fusion_direct.OpenFabricSettingsBuilder( + fusion_direct.OpenFabricS3DataStorageBuilder('b', 'k', 's', 'https://e').build(), + storage_mode=OpenFabricStorageMode.Bidirectional, + ).build() + self._init_global_admin(get_response=existing) + cloudfs.CloudDrives(self._global_admin).modify( + self._cloudfolder_name, self._local_user_account, open_fabric_settings=new_of) + self._global_admin.api.put.assert_called_once_with(f'/{self._cloudfolder_baseObjecrRef}', mock.ANY) + sent = self._global_admin.api.put.call_args[0][1] + self._assert_equal_objects(sent.openFabricSettings, new_of) + + def test_modify_open_fabric_rejects_false_open_storage_with_settings(self): + of_settings = fusion_direct.OpenFabricSettingsBuilder( + fusion_direct.OpenFabricS3DataStorageBuilder('b', 'k', 's', 'https://e').build(), + ).build() + mock_find = self.patch_call('cterasdk.core.cloudfs.CloudDrives.find') + mock_find.return_value = munch.Munch({'baseObjectRef': self._cloudfolder_baseObjecrRef}) + self._init_global_admin(get_response=munch.Munch({'baseObjectRef': self._cloudfolder_baseObjecrRef})) + with self.assertRaises(exceptions.CTERAException): + cloudfs.CloudDrives(self._global_admin).modify( + self._cloudfolder_name, self._local_user_account, + open_fabric_settings=of_settings, open_storage_enabled=False) + self._global_admin.api.put.assert_not_called() diff --git a/tests/ut/core/admin/test_open_fabric_builders.py b/tests/ut/core/admin/test_open_fabric_builders.py new file mode 100644 index 00000000..7f9e5ca3 --- /dev/null +++ b/tests/ut/core/admin/test_open_fabric_builders.py @@ -0,0 +1,61 @@ +import unittest + +from cterasdk.core import fusion_direct +from cterasdk.core.enum import OpenFabricStorageMode + + +class TestOpenFabricBuilders(unittest.TestCase): + + def test_secret_unchanged_sentinel(self): + self.assertEqual( + fusion_direct.FUSION_DIRECT_SECRET_KEY_UNCHANGED, + "*****DON'T CHANGE*****", + ) + + def test_s3_data_storage_defaults(self): + b = fusion_direct.OpenFabricS3DataStorageBuilder( + 'my-bucket', 'ak', 'sk', 'https://s3.example.com', + ).build() + self.assertEqual(b._classname, 'OpenFabricS3DataStorage') # pylint: disable=protected-access + self.assertEqual(b.storage, fusion_direct.DEFAULT_FUSION_DIRECT_S3_STORAGE) + self.assertEqual(b.bucket, 'my-bucket') + self.assertEqual(b.accessKey, 'ak') + self.assertEqual(b.secretKey, 'sk') + self.assertEqual(b.endPoint, 'https://s3.example.com') + self.assertTrue(b.useHttps) + self.assertFalse(b.trustAllCertificates) + self.assertFalse(b.usePathStyleAddressing) + self.assertFalse(b.metadataTags) + + def test_s3_data_storage_optional_fields(self): + b = fusion_direct.OpenFabricS3DataStorageBuilder( + 'b', 'ak', 'sk', 'http://minio:9000', + storage='MinIOS3', + use_https=False, + region='us-west-1', + trust_all_certificates=True, + use_path_style_addressing=True, + sqs_url='https://sqs.example.com/queue', + metadata_tags=True, + ).build() + self.assertEqual(b.storage, 'MinIOS3') + self.assertFalse(b.useHttps) + self.assertEqual(b.region, 'us-west-1') + self.assertTrue(b.trustAllCertificates) + self.assertTrue(b.usePathStyleAddressing) + self.assertEqual(b.sqsUrl, 'https://sqs.example.com/queue') + self.assertTrue(b.metadataTags) + + def test_open_fabric_settings_default_mode(self): + data = fusion_direct.OpenFabricS3DataStorageBuilder('b', 'ak', 'sk', 'https://e').build() + s = fusion_direct.OpenFabricSettingsBuilder(data).build() + self.assertEqual(s._classname, 'OpenFabricSettings') # pylint: disable=protected-access + self.assertEqual(s.storageMode, OpenFabricStorageMode.Bucket) + self.assertIs(s.dataStorage, data) + + def test_open_fabric_settings_explicit_mode(self): + data = fusion_direct.OpenFabricS3DataStorageBuilder('b', 'ak', 'sk', 'https://e').build() + s = fusion_direct.OpenFabricSettingsBuilder( + data, storage_mode=OpenFabricStorageMode.Bidirectional, + ).build() + self.assertEqual(s.storageMode, OpenFabricStorageMode.Bidirectional)