From ae4084b56115d7bc9a3bb556aa2c3de457a26095 Mon Sep 17 00:00:00 2001 From: Saimon Michelson Date: Sat, 13 Jun 2026 15:44:14 -0400 Subject: [PATCH 01/10] initial commit to support configuring shares from ctera portal --- cterasdk/cio/core/commands.py | 14 +++ cterasdk/cio/core/types.py | 2 +- cterasdk/clients/clients.py | 18 ++- cterasdk/convert/serializers.py | 15 ++- cterasdk/core/enum.py | 57 +++++++++ cterasdk/core/groups.py | 28 +++-- cterasdk/core/query.py | 45 +++++-- cterasdk/core/shares.py | 177 +++++++++++++++++++++++++++ cterasdk/core/types.py | 165 ++++++++++++++++++++++++- cterasdk/core/users.py | 31 +++-- cterasdk/edge/types.py | 2 - cterasdk/lib/__init__.py | 2 +- cterasdk/lib/iterator.py | 12 ++ cterasdk/objects/synchronous/core.py | 6 +- 14 files changed, 535 insertions(+), 39 deletions(-) create mode 100644 cterasdk/core/shares.py diff --git a/cterasdk/cio/core/commands.py b/cterasdk/cio/core/commands.py index da958147..3e3ac5fb 100644 --- a/cterasdk/cio/core/commands.py +++ b/cterasdk/cio/core/commands.py @@ -532,6 +532,20 @@ def _handle_response(self, r): return PortalResource.from_server_object(metadata) +class GetShareCandidate(GetProperties): + + def _handle_response(self, r): + candidate = r.root.gwShareManagementCandidate + return Object(**{ + 'full_path': candidate.fullPath, + 'display_path': candidate.displayPath, + 'cloud_folder_uid': candidate.cloudFolderUid, + 'has_acl': candidate.hasAcl, + 'web_dav_url': candidate.webDavUrl, + 'cloud_folder_name': r.root.name + }) + + class GetPermalink(GetMetadata): def _handle_response(self, r): diff --git a/cterasdk/cio/core/types.py b/cterasdk/cio/core/types.py index 151a7139..4cb6da1a 100644 --- a/cterasdk/cio/core/types.py +++ b/cterasdk/cio/core/types.py @@ -102,7 +102,7 @@ class GlobalAdminPath(PortalPath): @staticmethod def from_context(reference): - return ServicesPortalPath(GlobalAdminPath.Namespace, reference) + return GlobalAdminPath(GlobalAdminPath.Namespace, reference) class InvitationPath(PortalPath): diff --git a/cterasdk/clients/clients.py b/cterasdk/clients/clients.py index 9ed818c7..ab90b5ef 100644 --- a/cterasdk/clients/clients.py +++ b/cterasdk/clients/clients.py @@ -236,8 +236,8 @@ def multipart(self, path, form, *, on_response=None, on_error=None, **kwargs): return self.request(request, on_response=on_response, on_error=on_error) @decorators.authenticated - def delete(self, path, *, on_response=None, on_error=None, **kwargs): - request = async_requests.DeleteRequest(self._builder(path), **kwargs) + def delete(self, path, data=None, *, data_serializer=None, on_response=None, on_error=None, **kwargs): + request = async_requests.DeleteRequest(self._builder(path), data=data_serializer(data), **kwargs) return self.request(request, on_response=on_response, on_error=on_error) def _request(self, request, *, on_response=None, on_error=None): @@ -338,15 +338,21 @@ def get(self, path, **kwargs): return response.json() def put(self, path, data, **kwargs): - response = super().put(path, data, data_serializer=Serializers.JSON, on_error=JSONHandler(), **kwargs) + response = super().put(path, data, data_serializer=Serializers.JSON, headers={ + 'Content-Type': 'application/json' + }, on_error=JSONHandler(), **kwargs) return response.json() def post(self, path, data, **kwargs): - response = super().post(path, data, data_serializer=Serializers.JSON, on_error=JSONHandler(), **kwargs) + response = super().post(path, data, data_serializer=Serializers.JSON, headers={ + 'Content-Type': 'application/json' + }, on_error=JSONHandler(), **kwargs) return response.json() - def delete(self, path, **kwargs): - response = super().delete(path, on_error=JSONHandler(), **kwargs) + def delete(self, path, data=None, **kwargs): + response = super().delete(path, data, data_serializer=Serializers.JSON, headers={ + 'Content-Type': 'application/json' + }, on_error=JSONHandler(), **kwargs) return response.json() diff --git a/cterasdk/convert/serializers.py b/cterasdk/convert/serializers.py index 9ddca1ef..f15887d9 100644 --- a/cterasdk/convert/serializers.py +++ b/cterasdk/convert/serializers.py @@ -36,7 +36,18 @@ def _to_protected_dict(o): return ret -def tojsonstr(obj, pretty_print=True, no_log=True): +class Encoder(json.JSONEncoder): + + def default(self, o): + d = o.get('__dict__', None) + if d: + if '_classname' in d: + d['$class'] = d.pop('_classname') + return d + return super().default(o) + + +def tojsonstr(obj, pretty_print=True, no_log=False): """ Convert a Python object to a JSON string. @@ -49,7 +60,7 @@ def tojsonstr(obj, pretty_print=True, no_log=True): indent = 5 if pretty_print else None if no_log: return json.dumps(obj, default=_to_protected_dict, indent=indent) - return json.dumps(obj, default=lambda o: o.__dict__, indent=indent) + return json.dumps(obj, cls=Encoder, indent=indent) def toxmlstr(obj, pretty_print=False, no_log=False): diff --git a/cterasdk/core/enum.py b/cterasdk/core/enum.py index 80e7060a..3cd41690 100644 --- a/cterasdk/core/enum.py +++ b/cterasdk/core/enum.py @@ -752,3 +752,60 @@ class NativeFormat: Filesystem = 'Filesystem' Bucket = 'Bucket' Bidirectional = 'Bidirectional' + + +class ShareGroup: + """ + Grouping Criteria when Listing Network Shares + + :ivar str Path: Group by Path + :ivar str Edge Filer: Group by Edge Filers + """ + Path = 'path' + Edge = 'edgeFilers' + + +class ShareProtocol: + """ + Filter Shares by Protocol + + :ivar str NFS: NFS Shares + :ivar str SMB: SMB Shares + """ + SMB = 'smb' + NFS = 'nfs' + + +class PrincipalType: + """ + ACL Principal Type + + :ivar str LU: Local User + :ivar str LG: Local Group + :ivar str DU: Domain User + :ivar str DG: Domain Group + """ + LU = "localUser" + LG = "localGroup" + DU = "adUser" + DG = "adGroup" + + def from_account(account): + if account.account_type == PortalAccountType.User: + return PrincipalType.LU if account.is_local else PrincipalType.DU + if account.account_type == PortalAccountType.Group: + return PrincipalType.DU if account.is_local else PrincipalType.DG + raise ValueError(f'Unknown principal type: {account.account_type}') + + +class KRBSecurity: + """ + Kerberos Security. + + :ivar str KRB5: krb5 + :ivar str KRB5I: krb5i + :ivar str KRB5P: krb5p + """ + KRB5 = "krb5" + KRB5I = "krb5i" + KRB5P = "krb5p" diff --git a/cterasdk/core/groups.py b/cterasdk/core/groups.py index a8c45ab2..a7fa1853 100644 --- a/cterasdk/core/groups.py +++ b/cterasdk/core/groups.py @@ -46,30 +46,44 @@ def get(self, group_account, include=None): raise ObjectNotFoundException(baseurl) return group_object - def list_local_groups(self, include=None): + def list_local_groups(self, include=None, filters=None): """ List all local groups :param list[str] include: List of fields to retrieve, defaults to ['name'] + :param list[],optional filters: List of additional filters, defaults to None :return: Iterator for all local groups :rtype: cterasdk.lib.iterator.QueryIterator """ - include = union(include or [], Groups.default) - param = query.QueryParamBuilder().include(include).build() - return query.iterator(self._core, '/localGroups', param) + return self._groups(f'/localGroups', include, filters) - def list_domain_groups(self, domain, include=None): + def list_domain_groups(self, domain, include=None, filters=None): """ List all the groups in the domain :param str domain: Domain name :param list[str] include: List of fields to retrieve, defaults to ['name'] + :param list[],optional filters: List of additional filters, defaults to None :return: Iterator for all the domain groups :rtype: cterasdk.lib.iterator.QueryIterator """ + return self._groups(f'/domains/{domain}/adGroups', include, filters) + + def _groups(self, path, include, filters): + """ + List Groups. + + :param str path: Path + :param list[str],optional include: List of fields to retrieve, defaults to ['name'] + :param list[],optional filters: List of additional filters, defaults to None + """ include = union(include or [], Groups.default) - param = query.QueryParamBuilder().include(include).build() - return query.iterator(self._core, f'/domains/{domain}/adGroups', param) + builder = query.QueryParamBuilder().include(include) + for query_filter in filters: + builder.addFilter(query_filter) + builder.orFilter((len(filters) > 1)) + param = builder.build() + return query.iterator(self._core, path, param) def _members_reference(self, users): return [self._core.users.get(user, include=['baseObjectRef']).baseObjectRef for user in users] diff --git a/cterasdk/core/query.py b/cterasdk/core/query.py index 13e86b5d..6495e1bf 100644 --- a/cterasdk/core/query.py +++ b/cterasdk/core/query.py @@ -1,14 +1,14 @@ from datetime import datetime -from ..lib import QueryIterator, DefaultResponse, Command +from ..lib import QueryIterator, DefaultResponse, v2DefaultResponse, Command from ..common import Object def run(core, path, param): - return create_callback_function(core, path, callback_response=DefaultResponse)(param) + return v1_callback_function(core, path, callback_response=DefaultResponse)(param) -def create_callback_function(core, path, name=None, *, callback_response=None): +def v1_callback_function(core, path, name=None, *, callback_response=None): """ Create a query callback function @@ -29,22 +29,38 @@ def execute(core, path, name, param): return Command(execute if name else database, core, path, name or 'query') -def iterator(core, path, param=None, name=None, *, callback_response=None): +def v2_callback_function(core, path): + + def wrapper(core, path, params): + return v2DefaultResponse(core.clients.v2.get(path, params=dict(params))) + + return Command(wrapper, core, path) + + +def iterator(core, path, param=None, name=None, *, callback_response=None, version=None): """ Create iterator :param cterasdk.objects.core.Portal core: Portal object :param str path: URL Path :param str,optional name: Schema method name - :param cterasdk.core.query.QueryParams,optional param: Query paramter object + :param cterasdk.core.query.QueryParams or dict,optional param: + Query parameters, either as a ``QueryParams`` object or a dictionary of key-value pairs. :param cterasdk.lib.iterator.BaseResponse callback_response: Class to consume callback response + :param str,optional version: Iterator API Version :returns: Query iterator object """ - callback_response = callback_response if callback_response else DefaultResponse - callback_function = create_callback_function(core, path, name, callback_response=callback_response) - return QueryIterator(callback_function, param if param else QueryParams()) + if version != 'v2': + + callback_response = callback_response if callback_response else DefaultResponse + + callback_function = v1_callback_function(core, path, name, callback_response=callback_response) + + return QueryIterator(callback_function, param if param else QueryParams()) + + return QueryIterator(v2_callback_function(core, path), v2QueryParams(**param)) class Restriction: @@ -148,6 +164,19 @@ def setValue(self, value): return self.filter +class v2QueryParams(Object): + + def __init__(self, **kwargs): + super().__init__() + self.page = 0 + self.size = 150 + for key, value in kwargs.items(): + setattr(self, key, value) + + def increment(self): + self.page = self.page + 1 + + class QueryParams(Object): def __init__(self): diff --git a/cterasdk/core/shares.py b/cterasdk/core/shares.py new file mode 100644 index 00000000..09d50b4c --- /dev/null +++ b/cterasdk/core/shares.py @@ -0,0 +1,177 @@ +import logging +from .base_command import BaseCommand +from . import query +from .enum import ShareGroup, PrincipalType +from .types import Share, BlockRule +from .enum import Context +from ..edge.enum import Acl +from ..common import Object +from ..cio.core.commands import GetShareCandidate + + +logger = logging.getLogger('cterasdk.core') + + +class Shares(BaseCommand): + """ + Share Management APIs + """ + + def all(self, devices=None, protocol=None, search=None): + """ + List Shares. + + :param list[str], optional devices: Filter results to shares belonging to one or more Edge Filers. + :param cterasdk.core.enum.ShareProtocol, optional protocol: Filter results by share protocol. + :param str, optional search: Return only shares matching the specified search string. + + :returns: An iterator yielding share objects. + """ + params = {} + if devices: + params['group_by'] = ShareGroup.Edge + if protocol: + params['protocol'] = protocol + if search: + params['search'] = search + response = query.iterator(self._core, 'shareManagement/configurations/display', params, version='v2') + + def from_server_object(server_object): + return Share.from_server_object(server_object) + + for item in response: + if devices and item.group in devices: + yield item.group, [from_server_object(share) for share in item.shares] + yield from_server_object(item) + + def _users(self, domain, acl): + + def user_ace(user, perm): + param = Object() + param.permissions = perm + param.collaborator = user + param.collaborator._type = 'user' + param.collaborator.type = PrincipalType.DU + return param + + users = {ace.name: ace.perm for ace in acl} + + filters = [query.FilterBuilder('name').eq(name) for name in users.keys()] + + include = ['baseObjectRef', 'domain', 'email', 'firstName', 'lastName', 'name', 'uid', 'isTrashcan'] + + return [user_ace(user, users[user.name]) for user in self._core.users.list_domain_users(domain, include, filters)] if acl else [] + + def _groups(self, domain, acl): + + def group_ace(group, perm): + param = Object() + param.permissions = perm + param.collaborator = group + param.collaborator._type = 'group' + param.collaborator.type = PrincipalType.DG + return param + + groups = {ace.name: ace.perm for ace in acl} + + filters = [query.FilterBuilder('name').eq(name) for name in groups.keys()] + + include = ['baseObjectRef', 'domain', 'name', 'uid'] + + return [group_ace(group, groups[group.name]) for group in self._core.groups.list_domain_groups(domain, include, filters)] if acl else [] + + def _prepare_access_control_entries(self, acl, validate_acl): + + mapping = {ace.account.directory: ([], []) for ace in acl} # group principals by domain, and type + + access_control_entries = [] + + if validate_acl: + + for ace in acl: + + if ace.principal_type in [PrincipalType.DU]: + mapping[ace.account.directory][0].append(ace) + + if ace.principal_type in [PrincipalType.DG]: + mapping[ace.account.directory][1].append(ace) + + for domain, principals in mapping.items(): # for each domain, search for members and update access control entries + + users, groups = self._users(domain, principals[0]), self._groups(domain, principals[1]) + + access_control_entries.extend(users) + + access_control_entries.extend(groups) + + return access_control_entries + + return [ace.to_server_object() for ace in acl] + + def add(self, name, directory, devices, acl=None, description=None, access=Acl.WindowsNT, export_to_nfs=False, nfs_krb=False, + trusted_nfs_clients=None, krb_sec=None, block_files=None, export_to_ftp=False, validate_acl=True): + """ + Add Share. + + :param str name: Share name + :param str directory: Path + :param list[str] devices: Edge filers + :param list[cterasdk.core.types.ShareAccessControlEntry],optional acl: List of access control entries + :param str,optional description: Description + :param cterasdk.edge.enum.Acl,optional access: Windows File Sharing authentication mode, defaults to ``winAclMode`` + :param bool export_to_nfs: Whether to enable NFS access, defaults to ``False`` + :param list[cterasdk.core.types.NFSv3AccessControlEntry] trusted_nfs_clients: Trusted NFS v3 clients, defaults to ``None`` + :param list[cterasdk.core.enum.KRBSecurity],optional krb_sec: NFS Kerberos Security Priority + :param bool,optional export_to_ftp: Whether to enable FTP access, defaults to ``False`` + :param list[BlockRule],optional block_files: Screen file extensions. + :param bool,optional validate_acl: Validate ACLs with the CTERA Portal. Defaults to ``True``. + + :returns: Share ID + :rtype: str + """ + acl = self._prepare_access_control_entries(acl, validate_acl) + + def wrapper(core, param): + return core.api.execute('', 'fetchGwShareCandidates', param) + + if self._core.session().context == Context.admin: + directory = f'Users/{directory}' + + with GetShareCandidate(wrapper, self._core, directory) as metadata: + param = Object() + param.name = name + param.device_ids = [device.uid for device in self._core.devices.by_name(devices, include=['uid'])] + param.path_info = metadata + param.access_type = access + param.screened_file_types_enabled = False + param.screened_file_types_rules = None + param.acl_rules = acl + param.export_to_nfs = export_to_nfs + param.nfs_kerberos = nfs_krb + param.export_to_ftp = export_to_ftp + + if description: + param.description = description + + krb_sec = [krb_sec] if isinstance(krb_sec, str) else krb_sec + for label, value in zip(['first', 'second', 'third'], krb_sec or []): + setattr(param, f'nfs_sec_{label}', value) + + if trusted_nfs_clients: + param.trusted_nfs_clients = [network.to_server_object() for network in (trusted_nfs_clients or [])] + + param.screened_file_types_rules = [rule.to_server_object() for rule in block_files] if block_files else [BlockRule.default()] + + logger.info("Creating Share. %s", {'name': name}) + response = self._core.clients.v2.post('shareManagement/configurations', param) + logger.info("Share created. %s", {'name': name}) + return response.data.share_id + + def delete(self, *shares): + """ + Delete Shares. + + :param list[cterasdk.core.types.Share] or str shares: List of Shares objects, or unique identifers + """ + return self._core.clients.v2.delete('/shareManagement/configurations', + [share.id if isinstance(share, Share) else share for share in shares]) \ No newline at end of file diff --git a/cterasdk/core/types.py b/cterasdk/core/types.py index 8fd0f140..155c1195 100644 --- a/cterasdk/core/types.py +++ b/cterasdk/core/types.py @@ -4,7 +4,12 @@ from ..lib.storage import commonfs from .enum import PortalAccountType, CollaboratorType, FileAccessMode, PlanCriteria, TemplateCriteria, ProtectionLevel, \ - BucketType, LocationType, Platform, RetentionMode, Duration, ExtendedAttributes, ConflictHandler, NativeFormat + BucketType, LocationType, Platform, RetentionMode, Duration, ExtendedAttributes, ConflictHandler, NativeFormat, \ + PrincipalType + + +from ..edge.enum import FileAccessMode +from ..edge.types import AccessControlEntryValidator CloudFSFolderFindingHelper = namedtuple('CloudFSFolderFindingHelper', ('name', 'owner')) @@ -987,3 +992,161 @@ def __init__(self, access, is_dir): @staticmethod def from_server_object(server_object): return PortalInvitation(server_object.mode, server_object.isDirectory) + + +class Share(Object): + """ + Class for Portal Share + + :ivar str id: Share ID + :ivar str name: Name + :ivar str path: Path + :ivar list[str] protocols: Protocols + :ivar str access: Access Mode + :ivar list[str] devices: Devices + :ivar str absolute: Path, including '/cloud/users' namespace + """ + + def __init__(self, uid, name, path, protocols, access, devices): + super().__init__() + self.id = uid + self.name = name + self.path = path + self.protocols = protocols + self.access = access + self.devices = devices + + @property + def absolute(self): + return f'/cloud/users/{self.path}' + + @staticmethod + def from_server_object(server_object): + return Share(server_object.id, server_object.name, server_object.display_path, server_object.protocol, + server_object.access, server_object.edge_filers) + + +class ShareAccessControlEntry(): + """ + Share access control entry for Edge Filer shares + + :ivar cterasdk.core.types.PortalAccount account: Domain User or Group + :ivar cterasdk.edge.enum.FileAccessMode perm: Permission + """ + + def __init__(self, account, perm): + AccessControlEntryValidator.validate_permission(perm) + self._account = account + self._perm = perm + + @property + def account(self): + return self._account + + @property + def principal_type(self): + return PrincipalType.from_account(self._account) + + @property + def name(self): + return self._account.name + + @name.setter + def name(self, name): + self._account.name = name + + @property + def perm(self): + return self._perm + + @perm.setter + def perm(self, perm): + AccessControlEntryValidator.validate_permission(perm) + self._perm = perm + + def to_server_object(self): + ace = Object() + ace.permissions = self.perm + ace.manual_entry = Object() + ace.manual_entry.term = self.name + ace.manual_entry.type = self.principal_type + return ace + + +class NFSv3AccessControlEntry(): + """ + NFS v3 Access Control Entry + + :ivar str address: IP address, hostname or fully qualified domain name of client machine + :ivar str netmask: Subnet mask + :ivar cterasdk.edge.enum.FileAccessMode perm: File access permission + """ + + def __init__(self, address, netmask, perm): + AccessControlEntryValidator.validate_permission(perm) + self._address = address + self._netmask = netmask + self._perm = perm + + @property + def address(self): + return self._address + + @property + def netmask(self): + return self._netmask + + @property + def perm(self): + return self._perm + + @perm.setter + def perm(self, perm): + AccessControlEntryValidator.validate_permission(perm) + self._perm = perm + + @staticmethod + def from_server_object(server_object): + return NFSv3AccessControlEntry( + server_object.address, + server_object.netmask, + server_object.access_level, + ) + + def to_server_object(self): + param = Object() + param.access_level = self._perm + param.address = self._address + param.netmask = self._netmask + return param + + +class BlockRule: + """ + Block Extensions Rule for a Given Principal + """ + def __init__(self, account, extensions): + """ + :param cterasdk.core.types.PortalAccount account: Account + :param list[str]: File extensions. + """ + self.account = account + self.extensions = extensions + + @staticmethod + def default(): + param = Object() + param.screened_file_types = [] + param.collaborator = Object() + param.collaborator.name = 'Everyone' + param.collaborator.type = PrincipalType.LG + return param + + def to_server_object(self): + param = Object() + param.collaborator = Object() + param.collaborator.domain = self.account.directory + param.collaborator.name = self.account.name + param.collaborator.type = PrincipalType.from_account(self.account) + param.screened_file_types = [extension.lstrip('.') for extension in self.extensions] + return param diff --git a/cterasdk/core/users.py b/cterasdk/core/users.py index 5f2c811b..53ec21a6 100644 --- a/cterasdk/core/users.py +++ b/cterasdk/core/users.py @@ -50,30 +50,43 @@ def get(self, user_account, include=None): raise ObjectNotFoundException(baseurl) return user_object - def list_local_users(self, include=None): + def list_local_users(self, include=None, filters=None): """ List all local users - :param list[str] include: List of fields to retrieve, defaults to ['name'] + :param list[str],optional include: List of fields to retrieve, defaults to ['name'] :return: Iterator for all local users :rtype: cterasdk.lib.iterator.QueryIterator """ - include = union(include or [], Users.default) - param = query.QueryParamBuilder().include(include).build() - return query.iterator(self._core, '/users', param) + return self._users(f'/users', include, filters) - def list_domain_users(self, domain, include=None): + def list_domain_users(self, domain, include=None, filters=None): """ List all the users in the domain :param str domain: Domain name - :param list[str] include: List of fields to retrieve, defaults to ['name'] + :param list[str],optional include: List of fields to retrieve, defaults to ['name'] + :param list[],optional filters: List of additional filters, defaults to None :return: Iterator for all the domain users :rtype: cterasdk.lib.iterator.QueryIterator """ + return self._users(f'/domains/{domain}/adUsers', include, filters) + + def _users(self, path, include, filters): + """ + List Users. + + :param str path: Path + :param list[str],optional include: List of fields to retrieve, defaults to ['name'] + :param list[],optional filters: List of additional filters, defaults to None + """ include = union(include or [], Users.default) - param = query.QueryParamBuilder().include(include).build() - return query.iterator(self._core, f'/domains/{domain}/adUsers', param) + builder = query.QueryParamBuilder().include(include) + for query_filter in filters: + builder.addFilter(query_filter) + builder.orFilter((len(filters) > 1)) + param = builder.build() + return query.iterator(self._core, path, param) def add(self, name, email, first_name, last_name, password, role, company=None, comment=None, password_change=False): """ diff --git a/cterasdk/edge/types.py b/cterasdk/edge/types.py index c3812cba..7814b2cb 100644 --- a/cterasdk/edge/types.py +++ b/cterasdk/edge/types.py @@ -204,8 +204,6 @@ class ShareAccessControlEntry(): :ivar cterasdk.edge.enum.FileAccessMode perm: The file access permission """ - _valid_permissions = list({k: v for k, v in enum.FileAccessMode.__dict__.items() if not k.startswith('_')}.values()) - def __init__(self, principal_type, name, perm): AccessControlEntryValidator.validate_permission(perm) self._user_group_entry = UserGroupEntry(principal_type, name) diff --git a/cterasdk/lib/__init__.py b/cterasdk/lib/__init__.py index 88982a8a..4fdf6a00 100644 --- a/cterasdk/lib/__init__.py +++ b/cterasdk/lib/__init__.py @@ -2,6 +2,6 @@ from .consent import ask # noqa: E402, F401 from .tempfile import TempfileServices # noqa: E402, F401 from .iterator import QueryIterator, BaseResponse, \ - DefaultResponse, KeyValueQueryIterator, QueryLogsResponse, CursorResponse # noqa: E402, F401 + DefaultResponse, v2DefaultResponse, KeyValueQueryIterator, QueryLogsResponse, CursorResponse # noqa: E402, F401 from .tracker import track, ErrorStatus # noqa: E402, F401 from .crypto import CryptoServices, X509Certificate, PrivateKey, create_certificate_chain # noqa: E402, F401 diff --git a/cterasdk/lib/iterator.py b/cterasdk/lib/iterator.py index 9bc8258f..fd34333b 100644 --- a/cterasdk/lib/iterator.py +++ b/cterasdk/lib/iterator.py @@ -82,6 +82,18 @@ def objects(self): return self._response.objects +class v2DefaultResponse(BaseResponse): + + @property + def more(self): + return self._response.data.has_next + + @property + @abstractmethod + def objects(self): + return self._response.data.items + + class QueryLogsResponse(DefaultResponse): @property diff --git a/cterasdk/objects/synchronous/core.py b/cterasdk/objects/synchronous/core.py index eb77bee3..602318f1 100644 --- a/cterasdk/objects/synchronous/core.py +++ b/cterasdk/objects/synchronous/core.py @@ -9,7 +9,7 @@ activation, admins, antivirus, buckets, cli, cloudfs, connection, credentials, devices, directoryservice, domains, files, firmwares, groups, kms, licenses, login, logs, mail, messaging, plans, portals, reports, roles, servers, settings, - setup, ssl, startup, storage_classes, syslog, tasks, templates, users, + setup, shares, ssl, startup, storage_classes, syslog, tasks, templates, users, ) @@ -18,6 +18,7 @@ class Clients: def __init__(self, core): self.ctera = core.default.clone(clients.Extended, EndpointBuilder.new(core.base, core.context)) self.api = core.default.clone(clients.API, EndpointBuilder.new(core.base, core.context, '/api')) + self.v2 = core.default.clone(clients.JSON, EndpointBuilder.new(core.base, core.context, '/v2')) self.io = IO(core) @@ -67,6 +68,7 @@ def __init__(self, host, port=None, https=True): self.reports = reports.Reports(self) self.roles = roles.Roles(self) self.settings = settings.Settings(self) + self.shares = shares.Shares(self) self.storage_classes = storage_classes.StorageClasses(self) self.tasks = tasks.Tasks(self) self.templates = templates.Templates(self) @@ -113,7 +115,7 @@ def public_info(self): def _omit_fields(self): return super()._omit_fields + ['activation', 'admins', 'backups', 'cloudfs', 'credentials', 'devices', 'directoryservice', 'domains', 'files', 'firmwares', 'groups', 'logs', 'plans', 'reports', 'roles', 'settings', - 'storage_classes', 'tasks', 'templates', 'users'] + 'shares', 'storage_classes', 'tasks', 'templates', 'users'] class GlobalAdmin(Portal): # pylint: disable=too-many-instance-attributes From dfa276a7c834ff36360fbb9c2dc9b37d9402109b Mon Sep 17 00:00:00 2001 From: Saimon Michelson Date: Sat, 13 Jun 2026 15:53:08 -0400 Subject: [PATCH 02/10] add docs --- cterasdk/core/shares.py | 1 + .../UserGuides/Portal/Administration.rst | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/cterasdk/core/shares.py b/cterasdk/core/shares.py index 09d50b4c..f1cdbac7 100644 --- a/cterasdk/core/shares.py +++ b/cterasdk/core/shares.py @@ -26,6 +26,7 @@ def all(self, devices=None, protocol=None, search=None): :param str, optional search: Return only shares matching the specified search string. :returns: An iterator yielding share objects. + :rtype: generator[cterasdk.core.types.Share] """ params = {} if devices: diff --git a/docs/source/UserGuides/Portal/Administration.rst b/docs/source/UserGuides/Portal/Administration.rst index 85234c71..09c6f6fb 100644 --- a/docs/source/UserGuides/Portal/Administration.rst +++ b/docs/source/UserGuides/Portal/Administration.rst @@ -1542,6 +1542,49 @@ Code examples to create Cloud Drive folders using Fusion Direct admin.cloudfs.drives.setoacl(folders_paths, owner_sid, True) +Shares +------ + +.. automethod:: cterasdk.core.shares.Shares.all + :noindex: + + for share in admin.shares.all(): + print(share.name) + +.. automethod:: cterasdk.core.shares.Shares.add + :noindex: + +.. code:: python + + arose = core_types.UserAccount('arose', 'ad.local') + support = core_types.GroupAccount('Support', 'ad.local') + + acl = [ + core_types.ShareAccessControlEntry(arose, 'ReadWrite'), + core_types.ShareAccessControlEntry(support, 'ReadOnly') + ] + + trusted_nfs_clients = [ + core_types.NFSv3AccessControlEntry('192.168.0.1', '255.255.255.0', 'ReadWrite'), + core_types.NFSv3AccessControlEntry('192.168.1.1', '255.255.0.0', 'ReadOnly') + ] + + block_rules = [ + core_types.BlockRule(arose, ['exe', 'bat', 'cmd']) + ] + + admin.shares.add('ProjectX', 'Service Account/Root/ProjectX', ['demo-edge1'], acl, + export_to_nfs=True, nfs_krb=True, trusted_nfs_clients=trusted_nfs_clients, block_files=block_rules) + +.. automethod:: cterasdk.core.shares.Shares.delete + :noindex: + + admin.shares.delete('05db1713-842e-4f7b-ac27-43787f10c695') + + for share in admin.shares.all(): + admin.shares.delete(share) + + Global File Locking ------------------- From 4c65adc32764ac8291680c3c0c0dff31c377bfe8 Mon Sep 17 00:00:00 2001 From: Saimon Michelson Date: Sun, 14 Jun 2026 12:07:42 -0400 Subject: [PATCH 03/10] support modification of shares defined on ctera portal --- cterasdk/core/enum.py | 30 +++-------- cterasdk/core/query.py | 6 ++- cterasdk/core/shares.py | 117 ++++++++++++++++++++++++++++++++++------ cterasdk/core/types.py | 71 ++++++++++++++++++++++-- 4 files changed, 180 insertions(+), 44 deletions(-) diff --git a/cterasdk/core/enum.py b/cterasdk/core/enum.py index 3cd41690..76711b99 100644 --- a/cterasdk/core/enum.py +++ b/cterasdk/core/enum.py @@ -202,6 +202,14 @@ class CollaboratorType: DG = "adGroup" EXT = "external" + @staticmethod + def from_account(account): + if account.account_type == PortalAccountType.User: + return CollaboratorType.LU if account.is_local else CollaboratorType.DU + if account.account_type == PortalAccountType.Group: + return CollaboratorType.DU if account.is_local else CollaboratorType.DG + raise ValueError(f'Unknown principal type: {account.account_type}') + class PortalType: """ @@ -776,28 +784,6 @@ class ShareProtocol: NFS = 'nfs' -class PrincipalType: - """ - ACL Principal Type - - :ivar str LU: Local User - :ivar str LG: Local Group - :ivar str DU: Domain User - :ivar str DG: Domain Group - """ - LU = "localUser" - LG = "localGroup" - DU = "adUser" - DG = "adGroup" - - def from_account(account): - if account.account_type == PortalAccountType.User: - return PrincipalType.LU if account.is_local else PrincipalType.DU - if account.account_type == PortalAccountType.Group: - return PrincipalType.DU if account.is_local else PrincipalType.DG - raise ValueError(f'Unknown principal type: {account.account_type}') - - class KRBSecurity: """ Kerberos Security. diff --git a/cterasdk/core/query.py b/cterasdk/core/query.py index 6495e1bf..027cca34 100644 --- a/cterasdk/core/query.py +++ b/cterasdk/core/query.py @@ -4,8 +4,10 @@ from ..common import Object -def run(core, path, param): - return v1_callback_function(core, path, callback_response=DefaultResponse)(param) +def run(core, path, param, *, version=None): + if version != 'v2': + return v1_callback_function(core, path, callback_response=DefaultResponse)(param) + return v2_callback_function(core, path) def v1_callback_function(core, path, name=None, *, callback_response=None): diff --git a/cterasdk/core/shares.py b/cterasdk/core/shares.py index f1cdbac7..3e5a0136 100644 --- a/cterasdk/core/shares.py +++ b/cterasdk/core/shares.py @@ -1,22 +1,59 @@ import logging from .base_command import BaseCommand from . import query -from .enum import ShareGroup, PrincipalType -from .types import Share, BlockRule +from .enum import ShareGroup, CollaboratorType +from .types import Share, BlockRule, ShareInfo from .enum import Context from ..edge.enum import Acl from ..common import Object from ..cio.core.commands import GetShareCandidate +from ..exceptions import ObjectNotFoundException logger = logging.getLogger('cterasdk.core') +def wrapper(core, param): + return core.api.execute('', 'fetchGwShareCandidates', param) + + class Shares(BaseCommand): """ Share Management APIs """ + def _deduce_unique_identifier(self, share): + if isinstance(share, (Share, ShareInfo)): + return share.id + if isinstance(share, str): + return share + raise ValueError('Could not determine share identifier') + + def _get_configuration(self, share): + """ + Get Share + + :param cterasdk.core.types.Share or str: Share object or unique idenifier. + :returns: Share metadata + :rtype: cterasdk.common.object.Object + """ + uid = self._deduce_unique_identifier(share) + response = self._core.clients.v2.get(f'shareManagement/configurations/{uid}') + if response: + return response.data + raise ObjectNotFoundException(uid) + + def get(self, share): + """ + Get Share. + + :param cterasdk.core.types.Share or str: Share object or unique idenifier. + :returns: Share information + :rtype: cterasdk.core.types.ShareInfo + """ + response = self._get_configuration(share) + return ShareInfo.from_server_object(response) + def all(self, devices=None, protocol=None, search=None): """ List Shares. @@ -52,7 +89,7 @@ def user_ace(user, perm): param.permissions = perm param.collaborator = user param.collaborator._type = 'user' - param.collaborator.type = PrincipalType.DU + param.collaborator.type = CollaboratorType.DU return param users = {ace.name: ace.perm for ace in acl} @@ -70,7 +107,7 @@ def group_ace(group, perm): param.permissions = perm param.collaborator = group param.collaborator._type = 'group' - param.collaborator.type = PrincipalType.DG + param.collaborator.type = CollaboratorType.DG return param groups = {ace.name: ace.perm for ace in acl} @@ -91,10 +128,10 @@ def _prepare_access_control_entries(self, acl, validate_acl): for ace in acl: - if ace.principal_type in [PrincipalType.DU]: + if ace.principal_type in [CollaboratorType.DU]: mapping[ace.account.directory][0].append(ace) - if ace.principal_type in [PrincipalType.DG]: + if ace.principal_type in [CollaboratorType.DG]: mapping[ace.account.directory][1].append(ace) for domain, principals in mapping.items(): # for each domain, search for members and update access control entries @@ -109,6 +146,56 @@ def _prepare_access_control_entries(self, acl, validate_acl): return [ace.to_server_object() for ace in acl] + def modify(self, share, name=None, directory=None, devices=None, acl=None, description=None, access=None, export_to_nfs=None, + nfs_krb=None, trusted_nfs_clients=None, krb_sec=None, block_files=None, export_to_ftp=None, validate_acl=True): + param = self._get_configuration(share) + + if name: + param.name = name + + if directory: + param.path_info = GetShareCandidate(wrapper, self._core, directory).execute() + param.directory = param.path_info.full_path + + if devices: + param.device_ids = [device.uid for device in self._core.devices.by_name(devices, include=['uid'])] + else: + param.device_ids = [device.device_id for device in param.device_shares] + + if acl: + acl = self._prepare_access_control_entries(acl, validate_acl) + param.acl_rules = acl + + if trusted_nfs_clients: + param.trusted_nfs_clients = [network.to_server_object() for network in trusted_nfs_clients] + + if block_files: + param.screened_file_types_rules = [rule.to_server_object() for rule in block_files] if block_files else [BlockRule.default()] + param.screened_file_types_enabled = True + + if description: + param.description = description + + if access: + param.access = access + + if export_to_nfs is not None: + param.export_to_nfs = export_to_nfs + + if param.nfs_kerberos is not None: + param.nfs_kerberos = nfs_krb + + if export_to_ftp is not None: + param.export_to_ftp = export_to_ftp + + if krb_sec: + self._nfs_krb(param, krb_sec) + + logger.info("Modifying Share. %s", {'name': param.name}) + response = self._core.clients.v2.put(f'shareManagement/configurations/{param.id}', param) + logger.info("Share modified. %s", {'name': param.name}) + return response.data.share_id + def add(self, name, directory, devices, acl=None, description=None, access=Acl.WindowsNT, export_to_nfs=False, nfs_krb=False, trusted_nfs_clients=None, krb_sec=None, block_files=None, export_to_ftp=False, validate_acl=True): """ @@ -132,9 +219,6 @@ def add(self, name, directory, devices, acl=None, description=None, access=Acl.W """ acl = self._prepare_access_control_entries(acl, validate_acl) - def wrapper(core, param): - return core.api.execute('', 'fetchGwShareCandidates', param) - if self._core.session().context == Context.admin: directory = f'Users/{directory}' @@ -144,8 +228,6 @@ def wrapper(core, param): param.device_ids = [device.uid for device in self._core.devices.by_name(devices, include=['uid'])] param.path_info = metadata param.access_type = access - param.screened_file_types_enabled = False - param.screened_file_types_rules = None param.acl_rules = acl param.export_to_nfs = export_to_nfs param.nfs_kerberos = nfs_krb @@ -154,14 +236,13 @@ def wrapper(core, param): if description: param.description = description - krb_sec = [krb_sec] if isinstance(krb_sec, str) else krb_sec - for label, value in zip(['first', 'second', 'third'], krb_sec or []): - setattr(param, f'nfs_sec_{label}', value) + self._nfs_krb(param, krb_sec) if trusted_nfs_clients: param.trusted_nfs_clients = [network.to_server_object() for network in (trusted_nfs_clients or [])] param.screened_file_types_rules = [rule.to_server_object() for rule in block_files] if block_files else [BlockRule.default()] + param.screened_file_types_enabled = True if block_files else False logger.info("Creating Share. %s", {'name': name}) response = self._core.clients.v2.post('shareManagement/configurations', param) @@ -175,4 +256,10 @@ def delete(self, *shares): :param list[cterasdk.core.types.Share] or str shares: List of Shares objects, or unique identifers """ return self._core.clients.v2.delete('/shareManagement/configurations', - [share.id if isinstance(share, Share) else share for share in shares]) \ No newline at end of file + [self._deduce_unique_identifier(share) for share in shares]) + + def _nfs_krb(self, param, krb_sec): + if krb_sec: + krb_sec = [krb_sec] if isinstance(krb_sec, str) else krb_sec + for label, value in zip(['first', 'second', 'third'], krb_sec or []): + setattr(param, f'nfs_sec_{label}', value) diff --git a/cterasdk/core/types.py b/cterasdk/core/types.py index 155c1195..1da18ad3 100644 --- a/cterasdk/core/types.py +++ b/cterasdk/core/types.py @@ -1,11 +1,11 @@ from abc import ABC +from datetime import datetime from collections import namedtuple from ..common import DateTimeUtils, StringCriteriaBuilder, PredefinedListCriteriaBuilder, CustomListCriteriaBuilder, Object from ..lib.storage import commonfs from .enum import PortalAccountType, CollaboratorType, FileAccessMode, PlanCriteria, TemplateCriteria, ProtectionLevel, \ - BucketType, LocationType, Platform, RetentionMode, Duration, ExtendedAttributes, ConflictHandler, NativeFormat, \ - PrincipalType + BucketType, LocationType, Platform, RetentionMode, Duration, ExtendedAttributes, ConflictHandler, NativeFormat from ..edge.enum import FileAccessMode @@ -994,6 +994,62 @@ def from_server_object(server_object): return PortalInvitation(server_object.mode, server_object.isDirectory) +class ShareInfo: + + def __init__(self, uid, name, path, access, description, devices, acl, export_to_nfs, nfs_kerberos, + trusted_nfs_clients, export_to_ftp, created_at, updated_at): + self.id = uid + self.name = name + self.path = path + self.description = description + self.devices = devices + self.smb = Object() + self.smb.acl = acl + + self.nfs = Object() + self.nfs.enabled = export_to_nfs + self.nfs.acl = trusted_nfs_clients + self.nfs.kerberos = Object() + self.nfs.kerberos.enabled = nfs_kerberos + + self.ftp = Object() + self.ftp.enabled = export_to_ftp + self.created_at = created_at + self.updated_at = updated_at + + self.access = access + + @staticmethod + def from_server_object(server_object): + return ShareInfo( + server_object.id, + server_object.name, + server_object.path_info.display_path, + server_object.access_type, + getattr(server_object, 'description', None), + [device.device_name for device in server_object.device_shares], + [ShareAccessControlEntry.from_server_object(ace) for ace in server_object.acl_rules], + server_object.export_to_nfs, + server_object.nfs_kerberos, + [NFSv3AccessControlEntry.from_server_object(ace) for ace in server_object.trusted_nfs_clients], + server_object.export_to_ftp, + datetime.fromisoformat(server_object.created_at), + datetime.fromisoformat(server_object.updated_at) + ) + + def __repr__(self): + return str(self) + + def __str__(self): + return ( + f"{self.__class__.__name__}(" + f"{{'name': {self.name}, " + f"'created_at': {self.created_at.isoformat()}, " + f"'path': {self.path}, " + f"'devices': {self.devices}}})" + ) + + class Share(Object): """ Class for Portal Share @@ -1045,7 +1101,7 @@ def account(self): @property def principal_type(self): - return PrincipalType.from_account(self._account) + return CollaboratorType.from_account(self._account) @property def name(self): @@ -1072,6 +1128,11 @@ def to_server_object(self): ace.manual_entry.type = self.principal_type return ace + @staticmethod + def from_server_object(server_object): + account = PortalAccount.from_collaborator(server_object.collaborator) + return ShareAccessControlEntry(account, server_object.permissions) + class NFSv3AccessControlEntry(): """ @@ -1139,7 +1200,7 @@ def default(): param.screened_file_types = [] param.collaborator = Object() param.collaborator.name = 'Everyone' - param.collaborator.type = PrincipalType.LG + param.collaborator.type = CollaboratorType.LG return param def to_server_object(self): @@ -1147,6 +1208,6 @@ def to_server_object(self): param.collaborator = Object() param.collaborator.domain = self.account.directory param.collaborator.name = self.account.name - param.collaborator.type = PrincipalType.from_account(self.account) + param.collaborator.type = CollaboratorType.from_account(self.account) param.screened_file_types = [extension.lstrip('.') for extension in self.extensions] return param From 9cff6f638c15be3b1897479893bfd7cf1ee764fe Mon Sep 17 00:00:00 2001 From: Saimon Michelson Date: Sun, 14 Jun 2026 12:46:42 -0400 Subject: [PATCH 04/10] update to pass flake --- cterasdk/core/groups.py | 2 +- cterasdk/core/shares.py | 2 +- cterasdk/core/users.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cterasdk/core/groups.py b/cterasdk/core/groups.py index a7fa1853..7b587d60 100644 --- a/cterasdk/core/groups.py +++ b/cterasdk/core/groups.py @@ -55,7 +55,7 @@ def list_local_groups(self, include=None, filters=None): :return: Iterator for all local groups :rtype: cterasdk.lib.iterator.QueryIterator """ - return self._groups(f'/localGroups', include, filters) + return self._groups('/localGroups', include, filters) def list_domain_groups(self, domain, include=None, filters=None): """ diff --git a/cterasdk/core/shares.py b/cterasdk/core/shares.py index 3e5a0136..53c8c84f 100644 --- a/cterasdk/core/shares.py +++ b/cterasdk/core/shares.py @@ -109,7 +109,7 @@ def group_ace(group, perm): param.collaborator._type = 'group' param.collaborator.type = CollaboratorType.DG return param - + groups = {ace.name: ace.perm for ace in acl} filters = [query.FilterBuilder('name').eq(name) for name in groups.keys()] diff --git a/cterasdk/core/users.py b/cterasdk/core/users.py index 53ec21a6..2a4511a1 100644 --- a/cterasdk/core/users.py +++ b/cterasdk/core/users.py @@ -58,7 +58,7 @@ def list_local_users(self, include=None, filters=None): :return: Iterator for all local users :rtype: cterasdk.lib.iterator.QueryIterator """ - return self._users(f'/users', include, filters) + return self._users('/users', include, filters) def list_domain_users(self, domain, include=None, filters=None): """ From 8e653a9326a84bdbee642dbd0a3d7d06ce60a419 Mon Sep 17 00:00:00 2001 From: Saimon Michelson Date: Sun, 14 Jun 2026 12:49:19 -0400 Subject: [PATCH 05/10] flake8 --- cterasdk/core/shares.py | 5 ++++- cterasdk/core/types.py | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cterasdk/core/shares.py b/cterasdk/core/shares.py index 53c8c84f..9f283fd5 100644 --- a/cterasdk/core/shares.py +++ b/cterasdk/core/shares.py @@ -116,7 +116,10 @@ def group_ace(group, perm): include = ['baseObjectRef', 'domain', 'name', 'uid'] - return [group_ace(group, groups[group.name]) for group in self._core.groups.list_domain_groups(domain, include, filters)] if acl else [] + return [ + group_ace(group, groups[group.name]) + for group in self._core.groups.list_domain_groups(domain=domain, include=include, filters=filters) + ] def _prepare_access_control_entries(self, acl, validate_acl): diff --git a/cterasdk/core/types.py b/cterasdk/core/types.py index 1da18ad3..54ba4bcb 100644 --- a/cterasdk/core/types.py +++ b/cterasdk/core/types.py @@ -8,7 +8,6 @@ BucketType, LocationType, Platform, RetentionMode, Duration, ExtendedAttributes, ConflictHandler, NativeFormat -from ..edge.enum import FileAccessMode from ..edge.types import AccessControlEntryValidator From 0bca4b7e932861cc042ef67073e043e3cd44ecd6 Mon Sep 17 00:00:00 2001 From: Saimon Michelson Date: Sun, 14 Jun 2026 12:58:00 -0400 Subject: [PATCH 06/10] update to pass pylint --- cterasdk/clients/clients.py | 4 ++-- cterasdk/core/shares.py | 31 ++++++++++++++++++------------- cterasdk/core/types.py | 6 ++++-- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/cterasdk/clients/clients.py b/cterasdk/clients/clients.py index ab90b5ef..9b4980a1 100644 --- a/cterasdk/clients/clients.py +++ b/cterasdk/clients/clients.py @@ -325,8 +325,8 @@ def form_data(self, path, data, **kwargs): response = super().form_data(path, data, on_error=XMLHandler(), **kwargs) return response.xml() - def delete(self, path, **kwargs): - response = super().delete(path, on_error=XMLHandler(), **kwargs) + def delete(self, path, data=None,**kwargs): + response = super().delete(path, data, on_error=XMLHandler(), **kwargs) return response.xml() diff --git a/cterasdk/core/shares.py b/cterasdk/core/shares.py index 9f283fd5..bd5d2718 100644 --- a/cterasdk/core/shares.py +++ b/cterasdk/core/shares.py @@ -22,7 +22,8 @@ class Shares(BaseCommand): Share Management APIs """ - def _deduce_unique_identifier(self, share): + @staticmethod + def _deduce_unique_identifier(share): if isinstance(share, (Share, ShareInfo)): return share.id if isinstance(share, str): @@ -37,7 +38,7 @@ def _get_configuration(self, share): :returns: Share metadata :rtype: cterasdk.common.object.Object """ - uid = self._deduce_unique_identifier(share) + uid = Shares._deduce_unique_identifier(share) response = self._core.clients.v2.get(f'shareManagement/configurations/{uid}') if response: return response.data @@ -88,7 +89,7 @@ def user_ace(user, perm): param = Object() param.permissions = perm param.collaborator = user - param.collaborator._type = 'user' + param.collaborator._type = 'user' # pylint: disable=protected-access param.collaborator.type = CollaboratorType.DU return param @@ -106,7 +107,7 @@ def group_ace(group, perm): param = Object() param.permissions = perm param.collaborator = group - param.collaborator._type = 'group' + param.collaborator._type = 'group' # pylint: disable=protected-access param.collaborator.type = CollaboratorType.DG return param @@ -149,8 +150,10 @@ def _prepare_access_control_entries(self, acl, validate_acl): return [ace.to_server_object() for ace in acl] - def modify(self, share, name=None, directory=None, devices=None, acl=None, description=None, access=None, export_to_nfs=None, - nfs_krb=None, trusted_nfs_clients=None, krb_sec=None, block_files=None, export_to_ftp=None, validate_acl=True): + def modify(self, share, name=None, directory=None, devices=None, acl=None, + description=None, access=None, export_to_nfs=None, + nfs_krb=None, trusted_nfs_clients=None, krb_sec=None, block_files=None, + export_to_ftp=None, validate_acl=True): # pylint: disable=too-many-arguments, too-many-locals, too-many-branches param = self._get_configuration(share) if name: @@ -192,15 +195,16 @@ def modify(self, share, name=None, directory=None, devices=None, acl=None, descr param.export_to_ftp = export_to_ftp if krb_sec: - self._nfs_krb(param, krb_sec) + Shares._nfs_krb(param, krb_sec) logger.info("Modifying Share. %s", {'name': param.name}) response = self._core.clients.v2.put(f'shareManagement/configurations/{param.id}', param) logger.info("Share modified. %s", {'name': param.name}) return response.data.share_id - def add(self, name, directory, devices, acl=None, description=None, access=Acl.WindowsNT, export_to_nfs=False, nfs_krb=False, - trusted_nfs_clients=None, krb_sec=None, block_files=None, export_to_ftp=False, validate_acl=True): + def add(self, name, directory, devices, acl=None, description=None, + access=Acl.WindowsNT, export_to_nfs=False, nfs_krb=False, trusted_nfs_clients=None, krb_sec=None, block_files=None, + export_to_ftp=False, validate_acl=True): # pylint: disable=too-many-arguments, too-many-locals, too-many-branches """ Add Share. @@ -239,13 +243,13 @@ def add(self, name, directory, devices, acl=None, description=None, access=Acl.W if description: param.description = description - self._nfs_krb(param, krb_sec) + Shares._nfs_krb(param, krb_sec) if trusted_nfs_clients: param.trusted_nfs_clients = [network.to_server_object() for network in (trusted_nfs_clients or [])] param.screened_file_types_rules = [rule.to_server_object() for rule in block_files] if block_files else [BlockRule.default()] - param.screened_file_types_enabled = True if block_files else False + param.screened_file_types_enabled = bool(block_files) logger.info("Creating Share. %s", {'name': name}) response = self._core.clients.v2.post('shareManagement/configurations', param) @@ -259,9 +263,10 @@ def delete(self, *shares): :param list[cterasdk.core.types.Share] or str shares: List of Shares objects, or unique identifers """ return self._core.clients.v2.delete('/shareManagement/configurations', - [self._deduce_unique_identifier(share) for share in shares]) + [Shares._deduce_unique_identifier(share) for share in shares]) - def _nfs_krb(self, param, krb_sec): + @staticmethod + def _nfs_krb(param, krb_sec): if krb_sec: krb_sec = [krb_sec] if isinstance(krb_sec, str) else krb_sec for label, value in zip(['first', 'second', 'third'], krb_sec or []): diff --git a/cterasdk/core/types.py b/cterasdk/core/types.py index 54ba4bcb..93b56834 100644 --- a/cterasdk/core/types.py +++ b/cterasdk/core/types.py @@ -1,3 +1,5 @@ +# pylint: disable=too-many-lines + from abc import ABC from datetime import datetime from collections import namedtuple @@ -993,10 +995,10 @@ def from_server_object(server_object): return PortalInvitation(server_object.mode, server_object.isDirectory) -class ShareInfo: +class ShareInfo: # pylint: disable=too-many-instance-attributes def __init__(self, uid, name, path, access, description, devices, acl, export_to_nfs, nfs_kerberos, - trusted_nfs_clients, export_to_ftp, created_at, updated_at): + trusted_nfs_clients, export_to_ftp, created_at, updated_at): # pylint: disable=too-many-arguments self.id = uid self.name = name self.path = path From de987eb4c8a612cd9917fdd21a3d2e963f5a3043 Mon Sep 17 00:00:00 2001 From: Saimon Michelson Date: Sun, 14 Jun 2026 15:35:10 -0400 Subject: [PATCH 07/10] pass ut --- cterasdk/clients/clients.py | 2 +- cterasdk/core/groups.py | 7 ++++--- cterasdk/core/shares.py | 13 +++++++------ cterasdk/core/types.py | 4 ++-- cterasdk/core/users.py | 7 ++++--- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/cterasdk/clients/clients.py b/cterasdk/clients/clients.py index 9b4980a1..3df1e8ce 100644 --- a/cterasdk/clients/clients.py +++ b/cterasdk/clients/clients.py @@ -325,7 +325,7 @@ def form_data(self, path, data, **kwargs): response = super().form_data(path, data, on_error=XMLHandler(), **kwargs) return response.xml() - def delete(self, path, data=None,**kwargs): + def delete(self, path, data=None, **kwargs): response = super().delete(path, data, on_error=XMLHandler(), **kwargs) return response.xml() diff --git a/cterasdk/core/groups.py b/cterasdk/core/groups.py index 7b587d60..875e23f5 100644 --- a/cterasdk/core/groups.py +++ b/cterasdk/core/groups.py @@ -79,9 +79,10 @@ def _groups(self, path, include, filters): """ include = union(include or [], Groups.default) builder = query.QueryParamBuilder().include(include) - for query_filter in filters: - builder.addFilter(query_filter) - builder.orFilter((len(filters) > 1)) + if filters: + for query_filter in filters: + builder.addFilter(query_filter) + builder.orFilter((len(filters) > 1)) param = builder.build() return query.iterator(self._core, path, param) diff --git a/cterasdk/core/shares.py b/cterasdk/core/shares.py index bd5d2718..287da54f 100644 --- a/cterasdk/core/shares.py +++ b/cterasdk/core/shares.py @@ -150,10 +150,10 @@ def _prepare_access_control_entries(self, acl, validate_acl): return [ace.to_server_object() for ace in acl] - def modify(self, share, name=None, directory=None, devices=None, acl=None, - description=None, access=None, export_to_nfs=None, + def modify(self, share, name=None, directory=None, # pylint: disable=too-many-arguments, too-many-locals, too-many-branches + devices=None, acl=None, description=None, access=None, export_to_nfs=None, nfs_krb=None, trusted_nfs_clients=None, krb_sec=None, block_files=None, - export_to_ftp=None, validate_acl=True): # pylint: disable=too-many-arguments, too-many-locals, too-many-branches + export_to_ftp=None, validate_acl=True): param = self._get_configuration(share) if name: @@ -202,9 +202,10 @@ def modify(self, share, name=None, directory=None, devices=None, acl=None, logger.info("Share modified. %s", {'name': param.name}) return response.data.share_id - def add(self, name, directory, devices, acl=None, description=None, - access=Acl.WindowsNT, export_to_nfs=False, nfs_krb=False, trusted_nfs_clients=None, krb_sec=None, block_files=None, - export_to_ftp=False, validate_acl=True): # pylint: disable=too-many-arguments, too-many-locals, too-many-branches + def add(self, name, directory, devices, acl=None, # pylint: disable=too-many-arguments, too-many-locals, too-many-branches + description=None, access=Acl.WindowsNT, export_to_nfs=False, + nfs_krb=False, trusted_nfs_clients=None, krb_sec=None, block_files=None, + export_to_ftp=False, validate_acl=True): """ Add Share. diff --git a/cterasdk/core/types.py b/cterasdk/core/types.py index 93b56834..41f40ab1 100644 --- a/cterasdk/core/types.py +++ b/cterasdk/core/types.py @@ -997,8 +997,8 @@ def from_server_object(server_object): class ShareInfo: # pylint: disable=too-many-instance-attributes - def __init__(self, uid, name, path, access, description, devices, acl, export_to_nfs, nfs_kerberos, - trusted_nfs_clients, export_to_ftp, created_at, updated_at): # pylint: disable=too-many-arguments + def __init__(self, uid, name, path, access, description, devices, acl, # pylint: disable=too-many-arguments + export_to_nfs, nfs_kerberos, trusted_nfs_clients, export_to_ftp, created_at, updated_at): self.id = uid self.name = name self.path = path diff --git a/cterasdk/core/users.py b/cterasdk/core/users.py index 2a4511a1..103d53de 100644 --- a/cterasdk/core/users.py +++ b/cterasdk/core/users.py @@ -82,9 +82,10 @@ def _users(self, path, include, filters): """ include = union(include or [], Users.default) builder = query.QueryParamBuilder().include(include) - for query_filter in filters: - builder.addFilter(query_filter) - builder.orFilter((len(filters) > 1)) + if filters: + for query_filter in filters: + builder.addFilter(query_filter) + builder.orFilter((len(filters) > 1)) param = builder.build() return query.iterator(self._core, path, param) From b6e1ce9f3bbb28270f9925da07e01f93ccd9769c Mon Sep 17 00:00:00 2001 From: Saimon Michelson Date: Sun, 14 Jun 2026 17:03:46 -0400 Subject: [PATCH 08/10] add docs for modify and get methods --- docs/source/UserGuides/Portal/Administration.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/source/UserGuides/Portal/Administration.rst b/docs/source/UserGuides/Portal/Administration.rst index 09c6f6fb..2ae7701c 100644 --- a/docs/source/UserGuides/Portal/Administration.rst +++ b/docs/source/UserGuides/Portal/Administration.rst @@ -1551,6 +1551,9 @@ Shares for share in admin.shares.all(): print(share.name) +.. automethod:: cterasdk.core.shares.Shares.get + :noindex: + .. automethod:: cterasdk.core.shares.Shares.add :noindex: @@ -1576,6 +1579,9 @@ Shares admin.shares.add('ProjectX', 'Service Account/Root/ProjectX', ['demo-edge1'], acl, export_to_nfs=True, nfs_krb=True, trusted_nfs_clients=trusted_nfs_clients, block_files=block_rules) +.. automethod:: cterasdk.core.shares.Shares.modify + :noindex: + .. automethod:: cterasdk.core.shares.Shares.delete :noindex: From 79eee4127620726488744675e87e08c4eac4ebe5 Mon Sep 17 00:00:00 2001 From: Saimon Michelson Date: Sun, 21 Jun 2026 17:11:03 -0400 Subject: [PATCH 09/10] edge honeypot support --- cterasdk/edge/ransom_protect.py | 27 +++++++++++++++++++ docs/source/UserGuides/Edge/Configuration.rst | 9 +++++++ tests/ut/edge/test_ransom_protect.py | 21 +++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/cterasdk/edge/ransom_protect.py b/cterasdk/edge/ransom_protect.py index f9622e1a..8d836118 100644 --- a/cterasdk/edge/ransom_protect.py +++ b/cterasdk/edge/ransom_protect.py @@ -12,6 +12,10 @@ class RansomProtect(BaseCommand): Ransomware Protect APIs """ + def __init__(self, edge): + super().__init__(edge) + self.honeypot = Honeypot(self._edge) + def get_configuration(self): """ Get Ransom Protect Configuration @@ -72,3 +76,26 @@ def details(self, incident): param = query.QueryParamBuilder() param.put('incidentId', incident if isinstance(incident, int) else incident.incident_id) return query.iterator(self._edge, '/proc/rpsrv', param.build(), 'getIncidentDetails') + + +class Honeypot(BaseCommand): + + def enable(self): + """ + Enable Honeypot. + """ + self._edge.api.put('/config/ransomProtect/enableHoneypot', True) + + def is_enabled(self): + """ + Check if Honeypot is enabled. + + :rtype: bool + """ + return self._edge.api.get('/config/ransomProtect/enableHoneypot') + + def disable(self): + """ + Disable Honeypot. + """ + self._edge.api.put('/config/ransomProtect/enableHoneypot', False) diff --git a/docs/source/UserGuides/Edge/Configuration.rst b/docs/source/UserGuides/Edge/Configuration.rst index 96eb5333..d695dca3 100644 --- a/docs/source/UserGuides/Edge/Configuration.rst +++ b/docs/source/UserGuides/Edge/Configuration.rst @@ -1141,6 +1141,15 @@ Ransomware Protection .. automethod:: cterasdk.edge.ransom_protect.RansomProtect.details :noindex: +.. automethod:: cterasdk.edge.ransom_protect.Honeypot.enable + :noindex: + +.. automethod:: cterasdk.edge.ransom_protect.Honeypot.is_enabled + :noindex: + +.. automethod:: cterasdk.edge.ransom_protect.Honeypot.disable + :noindex: + Mail Server =========== diff --git a/tests/ut/edge/test_ransom_protect.py b/tests/ut/edge/test_ransom_protect.py index 2eb84a5b..345270f3 100644 --- a/tests/ut/edge/test_ransom_protect.py +++ b/tests/ut/edge/test_ransom_protect.py @@ -71,6 +71,27 @@ def test_modify_raise(self): ransom_protect.RansomProtect(self._filer).modify() self.assertEqual('Ransom Protect must be enabled to modify its configuration', str(error.exception)) + def test_enable_honeypot(self): + put_response = 'success' + self._init_filer(put_response=put_response) + ret = ransom_protect.Honeypot(self._filer).enable() + self._filer.api.put.assert_called_once_with('/config/ransomProtect/enableHoneypot', True) + self.assertEqual(ret, put_response) + + def test_disable_honeypot(self): + put_response = 'success' + self._init_filer(put_response=put_response) + ret = ransom_protect.Honeypot(self._filer).disable() + self._filer.api.put.assert_called_once_with('/config/ransomProtect/enableHoneypot', False) + self.assertEqual(ret, put_response) + + def test_is_enabled_honeypot(self): + get_response = True + self._init_filer(get_response=get_response) + ret = ransom_protect.Honeypot(self._filer).is_enabled() + self._filer.api.get.assert_called_once_with('/config/ransomProtect/enableHoneypot') + self.assertEqual(ret, get_response) + @staticmethod def _get_ransom_protect_config(block_users=None, detection_threshold=None, detection_interval=None): obj = munch.Munch({ From 8fd73acf360b1c7c0a7c4ac85309397a63380bf4 Mon Sep 17 00:00:00 2001 From: Saimon Michelson Date: Sun, 21 Jun 2026 17:35:04 -0400 Subject: [PATCH 10/10] update ut --- tests/ut/edge/test_ransom_protect.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/ut/edge/test_ransom_protect.py b/tests/ut/edge/test_ransom_protect.py index 345270f3..222394a6 100644 --- a/tests/ut/edge/test_ransom_protect.py +++ b/tests/ut/edge/test_ransom_protect.py @@ -74,16 +74,14 @@ def test_modify_raise(self): def test_enable_honeypot(self): put_response = 'success' self._init_filer(put_response=put_response) - ret = ransom_protect.Honeypot(self._filer).enable() + ransom_protect.Honeypot(self._filer).enable() self._filer.api.put.assert_called_once_with('/config/ransomProtect/enableHoneypot', True) - self.assertEqual(ret, put_response) def test_disable_honeypot(self): put_response = 'success' self._init_filer(put_response=put_response) - ret = ransom_protect.Honeypot(self._filer).disable() + ransom_protect.Honeypot(self._filer).disable() self._filer.api.put.assert_called_once_with('/config/ransomProtect/enableHoneypot', False) - self.assertEqual(ret, put_response) def test_is_enabled_honeypot(self): get_response = True