Skip to content

Commit 7763a0e

Browse files
committed
PIM-6485 Fusion Direct SDK API Coverage
1 parent 99746a1 commit 7763a0e

11 files changed

Lines changed: 526 additions & 26 deletions

File tree

cterasdk/common/utils.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44

55
from datetime import datetime
6-
from packaging.version import parse as parse_version
6+
from packaging.version import InvalidVersion, parse as parse_version
77

88
from .object import Object
99
from .enum import DayOfWeek
@@ -157,7 +157,11 @@ class Version:
157157
"""Software Version"""
158158

159159
def __init__(self, version):
160-
self._version = parse_version(version)
160+
try:
161+
self._version = parse_version(version)
162+
except (InvalidVersion, TypeError, ValueError):
163+
logger.warning('Unrecognized software version %r, using 0.0.0', version)
164+
self._version = parse_version('0.0.0')
161165

162166
def __eq__(self, v):
163167
return self._version == parse_version(v)

cterasdk/core/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
'directoryservice',
1010
'enum',
1111
'files',
12+
'fusion_direct',
1213
'login',
1314
'logs',
1415
'messaging',

cterasdk/core/cloudfs.py

Lines changed: 168 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,48 @@
44

55
from .base_command import BaseCommand
66
from . import query, devices
7-
from .enum import ListFilter, PolicyType
7+
from .enum import Duration, ListFilter, PolicyType
88
from .types import ComplianceSettingsBuilder, ExtendedAttributesBuilder
99
from ..common import union, Object
1010
from ..exceptions import CTERAException, ObjectNotFoundException
1111

1212

1313
logger = logging.getLogger('cterasdk.core')
1414

15+
# Suggested ``include`` paths when listing or fetching Fusion Direct cloud folders.
16+
# Portal response fields keep legacy names ``openStorageEnabled``, ``openFabricSettings``, etc.
17+
CLOUD_DRIVE_FUSION_DIRECT_INCLUDE = [
18+
'openStorageEnabled',
19+
'openFabricSettings',
20+
'openFabricStorageStatus',
21+
'openFabricStorageStatusDetails',
22+
]
23+
CLOUD_DRIVE_OPEN_FABRIC_INCLUDE = CLOUD_DRIVE_FUSION_DIRECT_INCLUDE # legacy alias
24+
25+
26+
def _worm_period(amount, duration_type):
27+
period = Object()
28+
period._classname = 'WormPeriod' # pylint: disable=protected-access
29+
period.amount = amount
30+
period.type = duration_type
31+
return period
32+
33+
34+
def _default_archive_settings():
35+
"""Defaults aligned with portal ``CloudDriveCreateParams`` / Java ``AdminApiOperations.createCloudFolderParams``.
36+
37+
Used when :meth:`CloudDrives.add` / :meth:`CloudDrives.add_return_id` are called without ``archive_settings``.
38+
"""
39+
arch = Object()
40+
arch._classname = 'ArchiveSettings' # pylint: disable=protected-access
41+
arch.archive = False
42+
arch.gracePeriod = _worm_period(0, Duration.Minutes)
43+
arch.retentionMode = 'Enterprise'
44+
arch.retentionPeriod = _worm_period(30, Duration.Days)
45+
arch.deleteData = False
46+
arch.seal = False
47+
return arch
48+
1549

1650
class CloudFS(BaseCommand):
1751
"""
@@ -149,29 +183,25 @@ def __init__(self, core):
149183
self.locks = Locks(self._core)
150184

151185
def _get_entire_object(self, name, owner):
152-
return self._core.api.get(f'{self.find(name, owner, include=["baseObjectRef"]).baseObjectRef}')
186+
# Fusion Direct fields (``openFabricSettings`` / ``openStorageEnabled``) for read-modify-write on PUT.
187+
include = union(CLOUD_DRIVE_FUSION_DIRECT_INCLUDE, ['baseObjectRef'])
188+
ref = self.find(name, owner, include=include).baseObjectRef
189+
return self._core.api.get(ref)
153190

154-
def add(self, name, group, owner, winacls=True, description=None, # pylint: disable=too-many-arguments
155-
quota=None, compliance_settings=None, xattrs=None, gfl=False, lock_extensions=None):
156-
"""
157-
Create a new Cloud Drive Folder (Cloud Volume)
191+
@staticmethod
192+
def _parse_add_cloud_drive_response(response):
193+
if isinstance(response, str):
194+
match = re.search(r'/Users/(.+)', response)
195+
if match:
196+
return match.group()
197+
return response
198+
return response
158199

159-
:param str name: Name of the new folder
160-
:param str group: Folder Group to assign this folder to
161-
:param cterasdk.core.types.UserAccount owner: User account, the owner of the new folder
162-
:param bool,optional winacls: Use Windows ACLs, defaults to True
163-
:param str,optional description: Cloud drive folder description
164-
:param str,optional quota: Cloud drive folder quota in GB
165-
:param cterasdk.common.object.Object,optional compliance_settings: Compliance settings, defaults to disabled.
166-
Use :func:`cterasdk.core.types.ComplianceSettingsBuilder` to build the compliance settings object
167-
:param cterasdk.common.object.Object,optional xattrs: Extended attributes, defaults to MacOS.
168-
Use :func:`cterasdk.core.types.ExtendedAttributesBuilder` to build the extended attributes object
169-
:param bool,optional gfl: Enable global file locking
170-
:param list[str],optional lock_extensions: List of file extensions (without leading dot) for which global file locking is enforced.
171-
:returns: Path to the Cloud Drive folder
172-
:rtype: str
173-
"""
200+
def _build_cloud_drive_create_params(self, name, group, owner, winacls=True, description=None, # pylint: disable=too-many-arguments
201+
quota=None, compliance_settings=None, xattrs=None, gfl=False, lock_extensions=None,
202+
archive_settings=None):
174203
param = Object()
204+
param._classname = 'CloudDriveCreateParams' # pylint: disable=protected-access
175205
param.name = name
176206
param.owner = self._core.users.get(owner, ['baseObjectRef']).baseObjectRef
177207
param.group = self._core.cloudfs.groups.get(group, ['baseObjectRef']).baseObjectRef
@@ -193,21 +223,124 @@ def add(self, name, group, owner, winacls=True, description=None, # pylint: dis
193223
else:
194224
param.extendedAttributes = ExtendedAttributesBuilder.default().build()
195225

226+
param.archiveSettings = archive_settings if archive_settings is not None else _default_archive_settings()
196227
if gfl:
197228
param.globalFileLockSettings = Object()
198229
param.globalFileLockSettings._classname = 'GlobalFileLockSettings' # pylint: disable=protected-access
199230
param.globalFileLockSettings.enabled = True
200231
param.globalFileLockSettings.globalFileLockExtensions = (
201232
lock_extensions if lock_extensions else CloudDrives.default_extensions
202233
)
234+
else:
235+
param.globalFileLockSettings = Object()
236+
param.globalFileLockSettings._classname = 'GlobalFileLockSettings' # pylint: disable=protected-access
237+
param.globalFileLockSettings.enabled = False
238+
param.globalFileLockSettings.globalFileLockExtensions = (
239+
lock_extensions if lock_extensions else CloudDrives.default_extensions
240+
)
241+
242+
return param
243+
244+
@staticmethod
245+
def _apply_fusion_direct_storage_params(param, open_fabric_settings, open_storage_enabled):
246+
if open_fabric_settings is not None:
247+
if open_storage_enabled is False:
248+
raise CTERAException(
249+
'openStorageEnabled cannot be False when openFabricSettings is set.'
250+
)
251+
param.openFabricSettings = open_fabric_settings
252+
param.openStorageEnabled = True if open_storage_enabled is None else bool(open_storage_enabled)
253+
elif open_storage_enabled is not None:
254+
param.openStorageEnabled = open_storage_enabled
255+
256+
@staticmethod
257+
def _apply_fusion_direct_storage_modify_params(param, open_fabric_settings, open_storage_enabled):
258+
if open_fabric_settings is not None:
259+
if open_storage_enabled is False:
260+
raise CTERAException(
261+
'openStorageEnabled cannot be False when openFabricSettings is set.'
262+
)
263+
param.openFabricSettings = open_fabric_settings
264+
if open_storage_enabled is not None:
265+
param.openStorageEnabled = bool(open_storage_enabled)
266+
elif open_storage_enabled is not None:
267+
param.openStorageEnabled = open_storage_enabled
268+
269+
def add(self, name, group, owner, winacls=True, description=None, # pylint: disable=too-many-arguments, too-many-locals
270+
quota=None, compliance_settings=None, xattrs=None, gfl=False, lock_extensions=None,
271+
open_fabric_settings=None, open_storage_enabled=None, archive_settings=None):
272+
"""
273+
Create a new Cloud Drive Folder (Cloud Volume)
274+
275+
:param str name: Name of the new folder
276+
:param str group: Folder Group to assign this folder to
277+
:param cterasdk.core.types.UserAccount owner: User account, the owner of the new folder
278+
:param bool,optional winacls: Use Windows ACLs, defaults to True
279+
:param str,optional description: Cloud drive folder description
280+
:param str,optional quota: Cloud drive folder quota in GB
281+
:param cterasdk.common.object.Object,optional compliance_settings: Compliance settings, defaults to disabled.
282+
Use :func:`cterasdk.core.types.ComplianceSettingsBuilder` to build the compliance settings object
283+
:param cterasdk.common.object.Object,optional xattrs: Extended attributes, defaults to MacOS.
284+
Use :func:`cterasdk.core.types.ExtendedAttributesBuilder` to build the extended attributes object
285+
:param bool,optional gfl: Enable global file locking
286+
:param list[str],optional lock_extensions: List of file extensions (without leading dot) for which global file locking is enforced.
287+
:param cterasdk.common.object.Object,optional open_fabric_settings: Fusion Direct payload: ``OpenFabricSettings`` object tree (e.g. from
288+
:class:`cterasdk.core.fusion_direct.OpenFabricSettingsBuilder`). When set, ``openStorageEnabled`` defaults to ``True``;
289+
passing ``open_storage_enabled=False`` raises :class:`cterasdk.exceptions.CTERAException`.
290+
:param bool,optional open_storage_enabled: When ``open_fabric_settings`` is omitted, sets ``openStorageEnabled`` on the
291+
create payload if not ``None``. Must not be ``False`` when ``open_fabric_settings`` is set.
292+
:param cterasdk.common.object.Object,optional archive_settings: ``ArchiveSettings`` object tree. When omitted, a portal-aligned
293+
default (archive off, enterprise retention) built by ``_default_archive_settings()`` is used.
294+
:returns: WebDAV-style ``/Users/...`` path string when the portal returns that form; otherwise the raw execute response.
295+
:rtype: str or object
296+
"""
297+
param = self._build_cloud_drive_create_params(
298+
name, group, owner, winacls, description, quota, compliance_settings, xattrs, gfl, lock_extensions,
299+
archive_settings=archive_settings)
300+
self._apply_fusion_direct_storage_params(param, open_fabric_settings, open_storage_enabled)
203301

204302
try:
205303
response = self._core.api.execute('', 'addCloudDrive', param)
206304
logger.info(
207305
'Cloud drive folder created. %s',
208306
{'name': name, 'owner': param.owner, 'folder_group': group, 'winacls': winacls}
209307
)
210-
return re.search(r'/Users\/(.+)', response).group()
308+
return CloudDrives._parse_add_cloud_drive_response(response)
309+
except CTERAException as error:
310+
logger.error(
311+
'Cloud drive folder creation failed. %s',
312+
{'name': name, 'folder_group': group, 'owner': str(owner), 'win_acls': winacls}
313+
)
314+
raise error
315+
316+
def add_return_id(self, name, group, owner, winacls=True, description=None, # pylint: disable=too-many-arguments, too-many-locals
317+
quota=None, compliance_settings=None, xattrs=None, gfl=False, lock_extensions=None,
318+
open_fabric_settings=None, open_storage_enabled=None, archive_settings=None):
319+
"""
320+
Create a Cloud Drive Folder using ``addCloudDriveReturnID`` (structured return codes and folder id).
321+
322+
Parameters match :meth:`add`. Inspect ``rc`` and ``folderEntry`` on the returned object for success or errors.
323+
324+
.. note::
325+
On some portal builds, a **global admin** session creating a folder for **another** user can leave the
326+
folder created in the database yet return HTTP 500, because the ReturnID response path resolves the new
327+
folder under the caller's principal instead of the owner. Prefer :meth:`add` in that scenario, or verify
328+
the folder in the UI / listing before retrying with the same name.
329+
330+
:returns: Parsed portal response (typically XML-backed object with ``rc``, ``folderEntry``, ``errorMsg``, etc.)
331+
"""
332+
param = self._build_cloud_drive_create_params(
333+
name, group, owner, winacls, description, quota, compliance_settings, xattrs, gfl, lock_extensions,
334+
archive_settings=archive_settings)
335+
self._apply_fusion_direct_storage_params(param, open_fabric_settings, open_storage_enabled)
336+
337+
try:
338+
response = self._core.api.execute('', 'addCloudDriveReturnID', param)
339+
logger.info(
340+
'Cloud drive folder create (return id). %s',
341+
{'name': name, 'owner': param.owner, 'folder_group': group, 'winacls': winacls}
342+
)
343+
return response
211344
except CTERAException as error:
212345
logger.error(
213346
'Cloud drive folder creation failed. %s',
@@ -217,10 +350,13 @@ def add(self, name, group, owner, winacls=True, description=None, # pylint: dis
217350

218351
def modify(self, current_name, owner, new_name=None, # pylint: disable=too-many-arguments, too-many-locals
219352
new_owner=None, new_group=None, description=None, winacls=None, quota=None, compliance_settings=None, xattrs=None,
220-
gfl=None, lock_extensions=None):
353+
gfl=None, lock_extensions=None, open_fabric_settings=None, open_storage_enabled=None):
221354
"""
222355
Modify a Cloud Drive Folder (Cloud Volume)
223356
357+
The folder is loaded with :data:`CLOUD_DRIVE_FUSION_DIRECT_INCLUDE` so
358+
``openFabricSettings`` / ``openStorageEnabled`` survive fields you do not change.
359+
224360
:param str current_name: Current folder name
225361
:param cterasdk.core.types.UserAccount owner: User account, the owner of the folder
226362
:param str,optional new_name: New folder name
@@ -235,6 +371,13 @@ def modify(self, current_name, owner, new_name=None, # pylint: disable=too-many
235371
Use :func:`cterasdk.core.types.ExtendedAttributesBuilder` to build the extended attributes object
236372
:param bool,optional gfl: Enable global file locking
237373
:param list[str],optional lock_extensions: List of file extensions (without leading dot) for which global file locking is enforced.
374+
:param cterasdk.common.object.Object,optional open_fabric_settings: Replacement Fusion Direct ``OpenFabricSettings`` object tree
375+
(e.g. from :class:`cterasdk.core.fusion_direct.OpenFabricSettingsBuilder`). The portal forbids changing S3 bucket or
376+
storage driver type after creation; ``storageMode`` and credentials may be updated within those rules.
377+
To keep the existing secret when rebuilding ``dataStorage``, set ``secretKey`` to
378+
:data:`cterasdk.core.fusion_direct.FUSION_DIRECT_SECRET_KEY_UNCHANGED`.
379+
:param bool,optional open_storage_enabled: When ``open_fabric_settings`` is omitted, sets ``openStorageEnabled`` if not ``None``.
380+
The portal rejects disabling Fusion Direct after creation; must not be ``False`` when ``open_fabric_settings`` is set.
238381
"""
239382
param = self._get_entire_object(current_name, owner)
240383
if new_name:
@@ -260,6 +403,8 @@ def modify(self, current_name, owner, new_name=None, # pylint: disable=too-many
260403
lock_extensions if lock_extensions else CloudDrives.default_extensions
261404
)
262405

406+
self._apply_fusion_direct_storage_modify_params(param, open_fabric_settings, open_storage_enabled)
407+
263408
try:
264409
response = self._core.api.put(f'/{param.baseObjectRef}', param)
265410
logger.info('Cloud drive folder updated. %s', {'name': current_name})

cterasdk/core/enum.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,17 @@ class ListFilter:
358358
NonDeleted = 'NonDeleted'
359359

360360

361+
class OpenFabricStorageMode:
362+
"""
363+
Fusion Direct storage mode values for :class:`cterasdk.core.fusion_direct.OpenFabricSettingsBuilder`.
364+
365+
Must match the portal schema enum name ``OpenFabricStorageMode`` (legacy identifier).
366+
"""
367+
Filesystem = 'Filesystem'
368+
Bucket = 'Bucket'
369+
Bidirectional = 'Bidirectional'
370+
371+
361372
class PlanCriteria:
362373
"""
363374
Subscription Plan Auto Assignment Rule Builder Criterias

0 commit comments

Comments
 (0)