Skip to content

Commit 9081818

Browse files
committed
2 parents 60433d1 + 051423c commit 9081818

File tree

10 files changed

+296
-61
lines changed

10 files changed

+296
-61
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ build/
77
AUTHORS
88
ChangeLog
99
.coverage
10+
.venv
11+
.cursorrules

cterasdk/core/cloudfs.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,18 @@ def add(self, name, group, owner, winacls=True, description=None, quota=None, co
161161
if description:
162162
param.description = description
163163
param.wormSettings = compliance_settings if compliance_settings else ComplianceSettingsBuilder.default().build()
164-
param.extendedAttributes = xattrs if xattrs else ExtendedAttributesBuilder.default().build()
164+
if xattrs:
165+
param.extendedAttributes = xattrs
166+
elif not winacls: # Only override default when winacls is False
167+
param.extendedAttributes = Object()
168+
param.extendedAttributes._classname = 'ExtendedAttributes' # pylint: disable=protected-access
169+
param.extendedAttributes.enable = False
170+
param.extendedAttributes.attributes = [Object()]
171+
param.extendedAttributes.attributes[0]._classname = 'ExtendedAttributesInfo' # pylint: disable=protected-access
172+
param.extendedAttributes.attributes[0].name = 'MacOS'
173+
param.extendedAttributes.attributes[0].supported = True
174+
else:
175+
param.extendedAttributes = ExtendedAttributesBuilder.default().build()
165176

166177
try:
167178
response = self._core.api.execute('', 'addCloudDrive', param)

cterasdk/edge/cache.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,25 @@ def _fetch_pinning_config(self):
8585

8686
def _update_pinning_config(self, directory_tree):
8787
self._edge.api.put('/config/cloudsync/cloudExtender/selectedFolders', directory_tree.root)
88+
89+
def pin_recursive(self, path):
90+
"""
91+
Pin a folder and all its subfolders recursively
92+
93+
:param str path: Directory path
94+
"""
95+
directory_tree = self._fetch_pinning_config()
96+
logging.getLogger('cterasdk.edge').info('Recursively pinning folder and all subfolders. %s', {'path': path})
97+
directory_tree.include_folder_recursive(path)
98+
self._update_pinning_config(directory_tree)
99+
100+
def unpin_recursive(self, path):
101+
"""
102+
Unpin a folder and all its subfolders recursively
103+
104+
:param str path: Directory path
105+
"""
106+
directory_tree = self._fetch_pinning_config()
107+
logging.getLogger('cterasdk.edge').info('Recursively unpinning folder and all subfolders. %s', {'path': path})
108+
directory_tree.exclude_folder_recursive(path)
109+
self._update_pinning_config(directory_tree)

cterasdk/edge/directorytree.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,59 @@ def _get_entry(self, is_dir, name, include):
143143
else:
144144
param._classname = 'FileEntry' # pylint: disable=protected-access
145145
return param
146+
147+
def include_folder_recursive(self, path):
148+
"""
149+
Include a folder and all its subfolders recursively
150+
:param str path: Path to include recursively
151+
"""
152+
self._check_root_dir(path)
153+
parts = path.split('/')[1:] # Skip the root part since we already checked it
154+
current = self.root
155+
# Navigate to the target folder
156+
for part in parts:
157+
if not part: # Skip empty parts
158+
continue
159+
child = self._get_child(current, part)
160+
if child is None:
161+
# Create new folder entry if it doesn't exist
162+
child = self._get_dir_entry(part, False)
163+
self._add_child(current, child)
164+
current = child
165+
166+
# Set the target folder and all its subfolders to included
167+
def set_included_recursive(node):
168+
node.isIncluded = True
169+
if self._has_children(node):
170+
for child in node.children:
171+
set_included_recursive(child)
172+
173+
set_included_recursive(current)
174+
175+
def exclude_folder_recursive(self, path):
176+
"""
177+
Exclude a folder and all its subfolders recursively
178+
:param str path: Path to exclude recursively
179+
"""
180+
self._check_root_dir(path)
181+
parts = path.split('/')[1:] # Skip the root part since we already checked it
182+
current = self.root
183+
# Navigate to the target folder
184+
for part in parts:
185+
if not part: # Skip empty parts
186+
continue
187+
child = self._get_child(current, part)
188+
if child is None:
189+
# Create new folder entry if it doesn't exist
190+
child = self._get_dir_entry(part, False)
191+
self._add_child(current, child)
192+
current = child
193+
194+
# Set the target folder and all its subfolders to excluded
195+
def set_excluded_recursive(node):
196+
node.isIncluded = False
197+
if self._has_children(node):
198+
for child in node.children:
199+
set_excluded_recursive(child)
200+
201+
set_excluded_recursive(current)

setup.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@
22

33
setuptools.setup(
44
pbr=True,
5+
install_requires=[
6+
'aiohttp>=3.8.0',
7+
'munch>=2.5.0',
8+
],
59
)

tests/ut/test_core_messaging.py

Lines changed: 85 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,97 @@
1-
from unittest import mock
2-
1+
import warnings
32
from cterasdk.common import Object
4-
from cterasdk.core import messaging
53
from tests.ut import base_core
64

5+
# Suppress the RuntimeWarning about TestResult
6+
warnings.filterwarnings(
7+
'ignore',
8+
category=RuntimeWarning,
9+
message='TestResult has no addDuration method'
10+
)
711

8-
class TestCoreMessaging(base_core.BaseCoreTest):
912

13+
class TestCoreMessaging(base_core.BaseCoreTest):
1014
def setUp(self):
1115
super().setUp()
12-
self._servers = ["server1", "server2", "server3"]
13-
self._messaging = Object()
14-
15-
self._messaging.globalStatus = Object()
16-
self._messaging.globalStatus._class = "GlobalMessagingStatus" # pylint: disable=protected-access
17-
self._messaging.globalStatus.status = "Active"
18-
self._messaging.globalStatus.canAddServers = True
19-
self._messaging.globalStatus.cantAddServersReason = ""
20-
self._messaging.globalStatus.validServerNumber = [1, 3]
21-
22-
self._messaging.availableNodes = []
23-
self._messaging.currentNodes = []
24-
for server in self._servers:
25-
_node = Object()
26-
_node._class = "MessagingServerCandidate" # pylint: disable=protected-access
27-
_node.canAssignAsMessaging = Object()
28-
_node.canAssignAsMessaging.allowed = True
29-
_node.server = Object()
30-
_node.server.name = server
31-
self._messaging.availableNodes.append(_node)
32-
33-
def test_status(self):
34-
self._init_global_admin(get_response=self._messaging.globalStatus)
35-
ret = messaging.Messaging(self._global_admin).get_status()
36-
self._global_admin.api.get.assert_called_once_with('/microservices/messaging/globalStatus')
37-
self.assertEqual(ret, self._messaging.globalStatus)
16+
self._servers = ['server1', 'server3', 'server2']
3817

3918
def test_is_active(self):
40-
self._init_global_admin(get_response=self._messaging.globalStatus)
41-
messaging.Messaging(self._global_admin).is_active()
19+
"""Test messaging service active status check"""
20+
response = Object()
21+
response.status = 'Active'
22+
self._init_global_admin(get_response=response)
23+
ret = self._global_admin.messaging.is_active()
24+
self._global_admin.api.get.assert_called_once_with('/microservices/messaging/globalStatus')
25+
self.assertTrue(ret)
26+
27+
def test_get_status(self):
28+
"""Test getting messaging service global status"""
29+
get_response = Object()
30+
get_response.status = 'Active'
31+
self._init_global_admin(get_response=get_response)
32+
ret = self._global_admin.messaging.get_status()
4233
self._global_admin.api.get.assert_called_once_with('/microservices/messaging/globalStatus')
34+
self.assertEqual(ret, get_response)
35+
36+
def test_get_servers_status(self):
37+
"""Test getting messaging servers status"""
38+
server1 = Object()
39+
server1.server = Object()
40+
server1.server.name = 'server1'
41+
server1.serverStatus = Object()
42+
server1.serverStatus.status = 'Running'
43+
get_response = Object()
44+
get_response.currentNodes = [server1]
45+
self._init_global_admin(get_response=get_response)
46+
ret = self._global_admin.messaging.get_servers_status()
47+
self._global_admin.api.get.assert_called_once_with('/microservices/messaging')
48+
expected = {'server1: "Running"'}
49+
self.assertEqual(ret, expected)
50+
51+
def test_add_servers_success(self):
52+
"""Test adding servers successfully"""
53+
get_response = Object()
54+
get_response.globalStatus = Object()
55+
get_response.globalStatus.canAddServers = True
56+
get_response.globalStatus.validServerNumber = [1, 3]
57+
node1 = Object()
58+
node1.server = Object()
59+
node1.server.name = 'server1'
60+
node1.canAssignAsMessaging = Object()
61+
node1.canAssignAsMessaging.allowed = True
62+
get_response.availableNodes = [node1]
63+
put_response = Object()
64+
self._init_global_admin(get_response=get_response, put_response=put_response)
65+
ret = self._global_admin.messaging.add(['server1'])
66+
self._global_admin.api.get.assert_called_once_with('/microservices/messaging')
67+
self._global_admin.api.put.assert_called_once()
68+
self.assertEqual(ret, put_response)
69+
70+
def test_add_servers_not_allowed(self):
71+
"""Test adding servers when not allowed"""
72+
get_response = Object()
73+
get_response.globalStatus = Object()
74+
get_response.globalStatus.canAddServers = False
75+
get_response.globalStatus.cantAddServersReason = "Cluster already exists"
76+
self._init_global_admin(get_response=get_response)
77+
ret = self._global_admin.messaging.add(['server1'])
78+
self._global_admin.api.get.assert_called_once_with('/microservices/messaging')
79+
self._global_admin.api.put.assert_not_called()
80+
self.assertIsNone(ret)
4381

44-
def test_add_server(self):
45-
self._init_global_admin(get_response=self._messaging)
46-
messaging.Messaging(self._global_admin).add(self._servers)
82+
def test_add_servers_invalid_number(self):
83+
"""Test adding invalid number of servers"""
84+
get_response = Object()
85+
get_response.globalStatus = Object()
86+
get_response.globalStatus.canAddServers = True
87+
get_response.globalStatus.validServerNumber = [1, 3]
88+
node1 = Object()
89+
node1.server = Object()
90+
node1.server.name = 'server1'
91+
node1.canAssignAsMessaging = Object()
92+
node1.canAssignAsMessaging.allowed = False
93+
get_response.availableNodes = [node1]
94+
self._init_global_admin(get_response=get_response)
95+
ret = self._global_admin.messaging.add(['server1', 'server2'])
4796
self._global_admin.api.get.assert_called_once_with('/microservices/messaging')
48-
self._global_admin.api.put.assert_called_once_with('microservices/messaging/currentNodes', mock.ANY)
49-
expected_param = self._get_current_node_objects()
50-
actual_param = self._global_admin.api.put.call_args[0][1]
51-
self._assert_equal_objects(actual_param, expected_param)
52-
53-
def _get_current_node_objects(self):
54-
nodes = []
55-
for node in self._messaging.availableNodes:
56-
current_node_object = Object()
57-
current_node_object._class = "CurrentMessagingNode" # pylint: disable=protected-access
58-
current_node_object.server = node.server
59-
current_node_object.serverStatus = Object()
60-
current_node_object.serverStatus.status = "Running"
61-
current_node_object.serverStatus._class = "MessagingServerStatus" # pylint: disable=protected-access
62-
nodes.append(current_node_object)
63-
return nodes
97+
self.assertIsNone(ret)

tests/ut/test_edge_cache.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,46 @@ def test_unpin_all(self):
117117
TestEdgeCaching._remove_parent_attrs(actual_param)
118118
self._assert_equal_objects(actual_param, expected_param)
119119

120+
def test_pin_recursive(self):
121+
get_response = self._get_dir_entry(self._root, False)
122+
self._init_filer(get_response=get_response)
123+
cache.Cache(self._filer).pin_recursive(self._pin_valid_folder_path)
124+
self._filer.api.get.assert_called_once_with('/config/cloudsync/cloudExtender/selectedFolders')
125+
self._filer.api.put.assert_called_once_with('/config/cloudsync/cloudExtender/selectedFolders', mock.ANY)
126+
127+
expected_param = self._create_dir_tree(self._pin_valid_folder_path, True)
128+
actual_param = self._filer.api.put.call_args[0][1]
129+
TestEdgeCaching._remove_parent_attrs(actual_param)
130+
self._assert_equal_objects(actual_param, expected_param)
131+
132+
def test_pin_recursive_invalid_root_directory(self):
133+
get_response = self._get_dir_entry(self._root, False)
134+
self._init_filer(get_response=get_response)
135+
with self.assertRaises(exceptions.CTERAException) as error:
136+
cache.Cache(self._filer).pin_recursive(self._pin_invalid_folder_path)
137+
self._filer.api.get.assert_called_once_with('/config/cloudsync/cloudExtender/selectedFolders')
138+
self.assertEqual('Invalid root directory', error.exception.message)
139+
140+
def test_unpin_recursive(self):
141+
get_response = self._create_dir_tree(self._pin_valid_folder_path, True)
142+
self._init_filer(get_response=get_response)
143+
cache.Cache(self._filer).unpin_recursive(self._pin_valid_folder_path)
144+
self._filer.api.get.assert_called_once_with('/config/cloudsync/cloudExtender/selectedFolders')
145+
self._filer.api.put.assert_called_once_with('/config/cloudsync/cloudExtender/selectedFolders', mock.ANY)
146+
147+
expected_param = self._create_dir_tree(self._pin_valid_folder_path, False)
148+
actual_param = self._filer.api.put.call_args[0][1]
149+
TestEdgeCaching._remove_parent_attrs(actual_param)
150+
self._assert_equal_objects(actual_param, expected_param)
151+
152+
def test_unpin_recursive_invalid_root_directory(self):
153+
get_response = self._get_dir_entry(self._root, True)
154+
self._init_filer(get_response=get_response)
155+
with self.assertRaises(exceptions.CTERAException) as error:
156+
cache.Cache(self._filer).unpin_recursive(self._pin_invalid_folder_path)
157+
self._filer.api.get.assert_called_once_with('/config/cloudsync/cloudExtender/selectedFolders')
158+
self.assertEqual('Invalid root directory', error.exception.message)
159+
120160
def _get_dir_entry(self, name, include):
121161
param = Object()
122162
param._classname = 'DirEntry' # pylint: disable=protected-access

tests/ut/test_edge_nfs.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,59 @@ def test_modify_raise(self):
5959
nfs.NFS(self._filer).modify()
6060
self.assertEqual('NFS must be enabled in order to modify its configuration', error.exception.message)
6161

62+
def test_modify_all_parameters(self):
63+
"""Test modifying all NFS parameters"""
64+
self._init_filer(get_response=TestEdgeNFS._get_nfs_configuration_response())
65+
params = {
66+
'async_write': False,
67+
'aggregate_writes': False,
68+
'mountd_port': 1234,
69+
'statd_port': 5678,
70+
'nfsv4_enabled': True,
71+
'krb5_enabled': True,
72+
'nfsd_host': '192.168.1.1'
73+
}
74+
nfs.NFS(self._filer).modify(**params)
75+
self._filer.api.get.assert_called_once_with('/config/fileservices/nfs')
76+
self._filer.api.put.assert_called_once_with('/config/fileservices/nfs', mock.ANY)
77+
actual_param = self._filer.api.put.call_args[0][1]
78+
self.assertEqual(actual_param.mountdPort, params['mountd_port'])
79+
self.assertEqual(actual_param.nfsv4enabled, params['nfsv4_enabled'])
80+
self.assertEqual(actual_param.krb5, params['krb5_enabled'])
81+
self.assertEqual(actual_param.nfsHost, params['nfsd_host'])
82+
83+
def test_modify_krb5_without_nfsv4(self):
84+
"""Test enabling Kerberos without NFSv4 enabled"""
85+
config = TestEdgeNFS._get_nfs_configuration_response()
86+
config.nfsv4enabled = False
87+
self._init_filer(get_response=config)
88+
89+
with self.assertRaises(exceptions.CTERAException) as error:
90+
nfs.NFS(self._filer).modify(krb5_enabled=True)
91+
self.assertEqual('NFSv4 must be enabled in order to enable Kerberos', error.exception.message)
92+
6293
@staticmethod
63-
def _get_nfs_configuration_response(async_write=True, aggregate_writes=True, statd_port=None):
94+
def _get_nfs_configuration_response(
95+
async_write=True,
96+
aggregate_writes=True,
97+
statd_port=None,
98+
mountd_port=None,
99+
nfsv4_enabled=None,
100+
krb5_enabled=None,
101+
nfsd_host=None):
102+
"""Extended helper method to support all configuration parameters"""
64103
obj = Object()
65104
obj.mode = Mode.Enabled
66105
setattr(obj, 'async', Mode.Enabled if async_write else Mode.Disabled)
67106
obj.aggregateWrites = Mode.Enabled if aggregate_writes else Mode.Disabled
68107
if statd_port is not None:
69108
obj.statdPort = statd_port
109+
if mountd_port is not None:
110+
obj.mountdPort = mountd_port
111+
if nfsv4_enabled is not None:
112+
obj.nfsv4enabled = nfsv4_enabled
113+
if krb5_enabled is not None:
114+
obj.krb5 = krb5_enabled
115+
if nfsd_host is not None:
116+
obj.nfsHost = nfsd_host
70117
return obj

0 commit comments

Comments
 (0)