]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/tests/test_ganesha.py
update source to Ceph Pacific 16.2.2
[ceph.git] / ceph / src / pybind / mgr / dashboard / tests / test_ganesha.py
CommitLineData
11fdf7f2 1# -*- coding: utf-8 -*-
f67539c2 2# pylint: disable=too-many-lines
11fdf7f2
TL
3from __future__ import absolute_import
4
5import unittest
f91f0fd5
TL
6from unittest.mock import MagicMock, Mock, patch
7from urllib.parse import urlencode
11fdf7f2 8
f91f0fd5
TL
9from ceph.deployment.service_spec import NFSServiceSpec
10from orchestrator import DaemonDescription, ServiceDescription
11fdf7f2 11
11fdf7f2 12from .. import mgr
f91f0fd5 13from ..controllers.nfsganesha import NFSGaneshaUi
11fdf7f2 14from ..services import ganesha
f91f0fd5
TL
15from ..services.ganesha import ClusterType, Export, GaneshaConf, GaneshaConfParser, NFSException
16from ..settings import Settings
17from . import ControllerTestCase # pylint: disable=no-name-in-module
18from . import KVStoreMockMixin # pylint: disable=no-name-in-module
11fdf7f2
TL
19
20
21class GaneshaConfTest(unittest.TestCase, KVStoreMockMixin):
f67539c2
TL
22 daemon_raw_config = """
23NFS_CORE_PARAM {
24 Enable_NLM = false;
25 Enable_RQUOTA = false;
26 Protocols = 4;
27 NFS_Port = 14000;
28 }
29
30 MDCACHE {
31 Dir_Chunk = 0;
32 }
33
34 NFSv4 {
35 RecoveryBackend = rados_cluster;
36 Minor_Versions = 1, 2;
37 }
38
39 RADOS_KV {
40 pool = nfs-ganesha;
41 namespace = vstart;
42 UserId = vstart;
43 nodeid = a;
44 }
45
46 RADOS_URLS {
47 Userid = vstart;
48 watch_url = 'rados://nfs-ganesha/vstart/conf-nfs.vstart';
49 }
50
51 %url rados://nfs-ganesha/vstart/conf-nfs.vstart
52"""
53
11fdf7f2
TL
54 export_1 = """
55EXPORT {
56 Export_ID=1;
57 Protocols = 4;
58 Path = /;
59 Pseudo = /cephfs_a/;
60 Access_Type = RW;
61 Protocols = 4;
62 Attr_Expiration_Time = 0;
63 # Delegations = R;
64 # Squash = root;
65
66 FSAL {
67 Name = CEPH;
68 Filesystem = "a";
69 User_Id = "ganesha";
70 # Secret_Access_Key = "YOUR SECRET KEY HERE";
71 }
72
73 CLIENT
74 {
75 Clients = 192.168.0.10, 192.168.1.0/8;
76 Squash = None;
77 }
78
79 CLIENT
80 {
81 Clients = 192.168.0.0/16;
82 Squash = All;
83 Access_Type = RO;
84 }
85}
86"""
87
88 export_2 = """
89EXPORT
90{
91 Export_ID=2;
92
93 Path = "/";
94
95 Pseudo = "/rgw";
96
97 Access_Type = RW;
98
99 squash = AllAnonymous;
100
101 Protocols = 4, 3;
102
103 Transports = TCP, UDP;
104
105 FSAL {
106 Name = RGW;
107 User_Id = "testuser";
108 Access_Key_Id ="access_key";
109 Secret_Access_Key = "secret_key";
110 }
111}
112"""
113
114 conf_nodea = '''
f91f0fd5 115%url "rados://ganesha/ns/export-2"
11fdf7f2
TL
116
117%url "rados://ganesha/ns/export-1"'''
118
119 conf_nodeb = '%url "rados://ganesha/ns/export-1"'
120
f91f0fd5
TL
121 conf_nfs_foo = '''
122%url "rados://ganesha2/ns2/export-1"
123
124%url "rados://ganesha2/ns2/export-2"'''
125
11fdf7f2
TL
126 class RObject(object):
127 def __init__(self, key, raw):
128 self.key = key
129 self.raw = raw
130
131 def read(self, _):
132 return self.raw.encode('utf-8')
133
134 def stat(self):
135 return len(self.raw), None
136
137 def _ioctx_write_full_mock(self, key, content):
f91f0fd5
TL
138 if key not in self.temp_store[self.temp_store_namespace]:
139 self.temp_store[self.temp_store_namespace][key] = \
140 GaneshaConfTest.RObject(key, content.decode('utf-8'))
11fdf7f2 141 else:
f91f0fd5 142 self.temp_store[self.temp_store_namespace][key].raw = content.decode('utf-8')
11fdf7f2
TL
143
144 def _ioctx_remove_mock(self, key):
f91f0fd5 145 del self.temp_store[self.temp_store_namespace][key]
11fdf7f2
TL
146
147 def _ioctx_list_objects_mock(self):
f91f0fd5
TL
148 return [obj for _, obj in self.temp_store[self.temp_store_namespace].items()]
149
150 def _ioctl_stat_mock(self, key):
151 return self.temp_store[self.temp_store_namespace][key].stat()
152
153 def _ioctl_read_mock(self, key, size):
154 return self.temp_store[self.temp_store_namespace][key].read(size)
155
156 def _ioctx_set_namespace_mock(self, namespace):
157 self.temp_store_namespace = namespace
11fdf7f2 158
f67539c2
TL
159 @staticmethod
160 def _set_user_defined_clusters_location(clusters_pool_namespace='ganesha/ns'):
161 Settings.GANESHA_CLUSTERS_RADOS_POOL_NAMESPACE = clusters_pool_namespace
162
11fdf7f2
TL
163 def setUp(self):
164 self.mock_kv_store()
165
f91f0fd5 166 self.clusters = {
f91f0fd5
TL
167 'foo': {
168 'pool': 'ganesha2',
169 'namespace': 'ns2',
170 'type': ClusterType.ORCHESTRATOR,
171 'daemon_conf': 'conf-nfs.foo',
172 'daemons': ['foo.host_a', 'foo.host_b'],
173 'exports': {
174 1: ['foo.host_a', 'foo.host_b'],
175 2: ['foo.host_a', 'foo.host_b'],
176 3: ['foo.host_a', 'foo.host_b'] # for new-added export
177 }
178 }
11fdf7f2
TL
179 }
180
f67539c2
TL
181 # Unset user-defined location.
182 self._set_user_defined_clusters_location('')
f91f0fd5
TL
183
184 self.temp_store_namespace = None
185 self._reset_temp_store()
186
11fdf7f2 187 self.io_mock = MagicMock()
f91f0fd5
TL
188 self.io_mock.set_namespace.side_effect = self._ioctx_set_namespace_mock
189 self.io_mock.read = self._ioctl_read_mock
190 self.io_mock.stat = self._ioctl_stat_mock
11fdf7f2
TL
191 self.io_mock.list_objects.side_effect = self._ioctx_list_objects_mock
192 self.io_mock.write_full.side_effect = self._ioctx_write_full_mock
193 self.io_mock.remove_object.side_effect = self._ioctx_remove_mock
194
195 ioctx_mock = MagicMock()
196 ioctx_mock.__enter__ = Mock(return_value=(self.io_mock))
197 ioctx_mock.__exit__ = Mock(return_value=None)
198
199 mgr.rados = MagicMock()
200 mgr.rados.open_ioctx.return_value = ioctx_mock
81eedcae 201
f91f0fd5 202 self._mock_orchestrator(True)
11fdf7f2
TL
203
204 ganesha.CephX = MagicMock()
205 ganesha.CephX.list_clients.return_value = ['ganesha']
206 ganesha.CephX.get_client_key.return_value = 'ganesha'
207
208 ganesha.CephFS = MagicMock()
209
f91f0fd5
TL
210 def _reset_temp_store(self):
211 self.temp_store_namespace = None
212 self.temp_store = {
213 'ns': {
214 'export-1': GaneshaConfTest.RObject("export-1", self.export_1),
215 'export-2': GaneshaConfTest.RObject("export-2", self.export_2),
216 'conf-nodea': GaneshaConfTest.RObject("conf-nodea", self.conf_nodea),
217 'conf-nodeb': GaneshaConfTest.RObject("conf-nodeb", self.conf_nodeb),
218 },
219 'ns2': {
220 'export-1': GaneshaConfTest.RObject("export-1", self.export_1),
221 'export-2': GaneshaConfTest.RObject("export-2", self.export_2),
222 'conf-nfs.foo': GaneshaConfTest.RObject("conf-nfs.foo", self.conf_nfs_foo)
223 }
224 }
225
226 def _mock_orchestrator(self, enable):
227 # mock nfs services
228 cluster_info = self.clusters['foo']
229 orch_nfs_services = [
230 ServiceDescription(spec=NFSServiceSpec(service_id='foo',
231 pool=cluster_info['pool'],
232 namespace=cluster_info['namespace']))
233 ] if enable else []
234 # pylint: disable=protected-access
235 ganesha.Ganesha._get_orch_nfs_services = Mock(return_value=orch_nfs_services)
236
237 # mock nfs daemons
238 def _get_nfs_instances(service_name=None):
239 if not enable:
240 return []
241 instances = {
242 'nfs.foo': [
243 DaemonDescription(daemon_id='foo.host_a', status=1),
244 DaemonDescription(daemon_id='foo.host_b', status=1)
245 ],
246 'nfs.bar': [
247 DaemonDescription(daemon_id='bar.host_c', status=1)
248 ]
249 }
250 if service_name is not None:
251 return instances[service_name]
252 result = []
253 for _, daemons in instances.items():
254 result.extend(daemons)
255 return result
256 ganesha.GaneshaConfOrchestrator._get_orch_nfs_instances = Mock(
257 side_effect=_get_nfs_instances)
258
f67539c2
TL
259 def test_parse_daemon_raw_config(self):
260 expected_daemon_config = [
261 {
262 "block_name": "NFS_CORE_PARAM",
263 "enable_nlm": False,
264 "enable_rquota": False,
265 "protocols": 4,
266 "nfs_port": 14000
267 },
268 {
269 "block_name": "MDCACHE",
270 "dir_chunk": 0
271 },
272 {
273 "block_name": "NFSV4",
274 "recoverybackend": "rados_cluster",
275 "minor_versions": [1, 2]
276 },
277 {
278 "block_name": "RADOS_KV",
279 "pool": "nfs-ganesha",
280 "namespace": "vstart",
281 "userid": "vstart",
282 "nodeid": "a"
283 },
284 {
285 "block_name": "RADOS_URLS",
286 "userid": "vstart",
287 "watch_url": "'rados://nfs-ganesha/vstart/conf-nfs.vstart'"
288 },
289 {
290 "block_name": "%url",
291 "value": "rados://nfs-ganesha/vstart/conf-nfs.vstart"
292 }
293 ]
294 daemon_config = GaneshaConfParser(self.daemon_raw_config).parse()
295 self.assertEqual(daemon_config, expected_daemon_config)
296
11fdf7f2
TL
297 def test_export_parser_1(self):
298 blocks = GaneshaConfParser(self.export_1).parse()
299 self.assertIsInstance(blocks, list)
300 self.assertEqual(len(blocks), 1)
301 export = Export.from_export_block(blocks[0], '_default_',
302 GaneshaConf.ganesha_defaults({}))
303
304 self.assertEqual(export.export_id, 1)
305 self.assertEqual(export.path, "/")
306 self.assertEqual(export.pseudo, "/cephfs_a")
307 self.assertIsNone(export.tag)
308 self.assertEqual(export.access_type, "RW")
309 self.assertEqual(export.squash, "root_squash")
310 self.assertEqual(export.protocols, {4})
311 self.assertEqual(export.transports, {"TCP", "UDP"})
312 self.assertEqual(export.fsal.name, "CEPH")
313 self.assertEqual(export.fsal.user_id, "ganesha")
314 self.assertEqual(export.fsal.fs_name, "a")
315 self.assertEqual(export.fsal.sec_label_xattr, None)
316 self.assertEqual(len(export.clients), 2)
317 self.assertEqual(export.clients[0].addresses,
318 ["192.168.0.10", "192.168.1.0/8"])
319 self.assertEqual(export.clients[0].squash, "no_root_squash")
320 self.assertIsNone(export.clients[0].access_type)
321 self.assertEqual(export.clients[1].addresses, ["192.168.0.0/16"])
322 self.assertEqual(export.clients[1].squash, "all_squash")
323 self.assertEqual(export.clients[1].access_type, "RO")
324 self.assertEqual(export.cluster_id, '_default_')
325 self.assertEqual(export.attr_expiration_time, 0)
326 self.assertEqual(export.security_label, False)
327
328 def test_export_parser_2(self):
329 blocks = GaneshaConfParser(self.export_2).parse()
330 self.assertIsInstance(blocks, list)
331 self.assertEqual(len(blocks), 1)
332 export = Export.from_export_block(blocks[0], '_default_',
333 GaneshaConf.ganesha_defaults({}))
334
335 self.assertEqual(export.export_id, 2)
336 self.assertEqual(export.path, "/")
337 self.assertEqual(export.pseudo, "/rgw")
338 self.assertIsNone(export.tag)
339 self.assertEqual(export.access_type, "RW")
340 self.assertEqual(export.squash, "all_squash")
341 self.assertEqual(export.protocols, {4, 3})
342 self.assertEqual(export.transports, {"TCP", "UDP"})
343 self.assertEqual(export.fsal.name, "RGW")
344 self.assertEqual(export.fsal.rgw_user_id, "testuser")
345 self.assertEqual(export.fsal.access_key, "access_key")
346 self.assertEqual(export.fsal.secret_key, "secret_key")
347 self.assertEqual(len(export.clients), 0)
348 self.assertEqual(export.cluster_id, '_default_')
349
350 def test_daemon_conf_parser_a(self):
351 blocks = GaneshaConfParser(self.conf_nodea).parse()
352 self.assertIsInstance(blocks, list)
353 self.assertEqual(len(blocks), 2)
354 self.assertEqual(blocks[0]['block_name'], "%url")
355 self.assertEqual(blocks[0]['value'], "rados://ganesha/ns/export-2")
356 self.assertEqual(blocks[1]['block_name'], "%url")
357 self.assertEqual(blocks[1]['value'], "rados://ganesha/ns/export-1")
358
359 def test_daemon_conf_parser_b(self):
360 blocks = GaneshaConfParser(self.conf_nodeb).parse()
361 self.assertIsInstance(blocks, list)
362 self.assertEqual(len(blocks), 1)
363 self.assertEqual(blocks[0]['block_name'], "%url")
364 self.assertEqual(blocks[0]['value'], "rados://ganesha/ns/export-1")
365
366 def test_ganesha_conf(self):
f91f0fd5
TL
367 for cluster_id, info in self.clusters.items():
368 self._do_test_ganesha_conf(cluster_id, info['exports'])
369 self._reset_temp_store()
370
371 def _do_test_ganesha_conf(self, cluster, expected_exports):
372 ganesha_conf = GaneshaConf.instance(cluster)
11fdf7f2
TL
373 exports = ganesha_conf.exports
374
375 self.assertEqual(len(exports.items()), 2)
376 self.assertIn(1, exports)
377 self.assertIn(2, exports)
378
379 # export_id = 1 asserts
380 export = exports[1]
381 self.assertEqual(export.export_id, 1)
382 self.assertEqual(export.path, "/")
383 self.assertEqual(export.pseudo, "/cephfs_a")
384 self.assertIsNone(export.tag)
385 self.assertEqual(export.access_type, "RW")
386 self.assertEqual(export.squash, "root_squash")
387 self.assertEqual(export.protocols, {4})
388 self.assertEqual(export.transports, {"TCP", "UDP"})
389 self.assertEqual(export.fsal.name, "CEPH")
390 self.assertEqual(export.fsal.user_id, "ganesha")
391 self.assertEqual(export.fsal.fs_name, "a")
392 self.assertEqual(export.fsal.sec_label_xattr, None)
393 self.assertEqual(len(export.clients), 2)
394 self.assertEqual(export.clients[0].addresses,
395 ["192.168.0.10", "192.168.1.0/8"])
396 self.assertEqual(export.clients[0].squash, "no_root_squash")
397 self.assertIsNone(export.clients[0].access_type)
398 self.assertEqual(export.clients[1].addresses, ["192.168.0.0/16"])
399 self.assertEqual(export.clients[1].squash, "all_squash")
400 self.assertEqual(export.clients[1].access_type, "RO")
401 self.assertEqual(export.attr_expiration_time, 0)
402 self.assertEqual(export.security_label, False)
f91f0fd5 403 self.assertSetEqual(export.daemons, set(expected_exports[1]))
11fdf7f2
TL
404
405 # export_id = 2 asserts
406 export = exports[2]
407 self.assertEqual(export.export_id, 2)
408 self.assertEqual(export.path, "/")
409 self.assertEqual(export.pseudo, "/rgw")
410 self.assertIsNone(export.tag)
411 self.assertEqual(export.access_type, "RW")
412 self.assertEqual(export.squash, "all_squash")
413 self.assertEqual(export.protocols, {4, 3})
414 self.assertEqual(export.transports, {"TCP", "UDP"})
415 self.assertEqual(export.fsal.name, "RGW")
416 self.assertEqual(export.fsal.rgw_user_id, "testuser")
417 self.assertEqual(export.fsal.access_key, "access_key")
418 self.assertEqual(export.fsal.secret_key, "secret_key")
419 self.assertEqual(len(export.clients), 0)
f91f0fd5 420 self.assertSetEqual(export.daemons, set(expected_exports[2]))
11fdf7f2
TL
421
422 def test_config_dict(self):
f91f0fd5
TL
423 for cluster_id, info in self.clusters.items():
424 self._do_test_config_dict(cluster_id, info['exports'])
425 self._reset_temp_store()
426
427 def _do_test_config_dict(self, cluster, expected_exports):
428 conf = GaneshaConf.instance(cluster)
11fdf7f2
TL
429 export = conf.exports[1]
430 ex_dict = export.to_dict()
431 self.assertDictEqual(ex_dict, {
f91f0fd5 432 'daemons': expected_exports[1],
11fdf7f2
TL
433 'export_id': 1,
434 'path': '/',
435 'pseudo': '/cephfs_a',
f91f0fd5 436 'cluster_id': cluster,
11fdf7f2
TL
437 'tag': None,
438 'access_type': 'RW',
439 'squash': 'root_squash',
440 'security_label': False,
441 'protocols': [4],
442 'transports': ['TCP', 'UDP'],
443 'clients': [{
444 'addresses': ["192.168.0.10", "192.168.1.0/8"],
445 'access_type': None,
446 'squash': 'no_root_squash'
447 }, {
448 'addresses': ["192.168.0.0/16"],
449 'access_type': 'RO',
450 'squash': 'all_squash'
451 }],
452 'fsal': {
453 'name': 'CEPH',
454 'user_id': 'ganesha',
455 'fs_name': 'a',
456 'sec_label_xattr': None
457 }
458 })
459
460 export = conf.exports[2]
461 ex_dict = export.to_dict()
462 self.assertDictEqual(ex_dict, {
f91f0fd5 463 'daemons': expected_exports[2],
11fdf7f2
TL
464 'export_id': 2,
465 'path': '/',
466 'pseudo': '/rgw',
f91f0fd5 467 'cluster_id': cluster,
11fdf7f2
TL
468 'tag': None,
469 'access_type': 'RW',
470 'squash': 'all_squash',
471 'security_label': False,
472 'protocols': [3, 4],
473 'transports': ['TCP', 'UDP'],
474 'clients': [],
475 'fsal': {
476 'name': 'RGW',
477 'rgw_user_id': 'testuser'
478 }
479 })
480
481 def test_config_from_dict(self):
f91f0fd5
TL
482 for cluster_id, info in self.clusters.items():
483 self._do_test_config_from_dict(cluster_id, info['exports'])
484 self._reset_temp_store()
485
486 def _do_test_config_from_dict(self, cluster_id, expected_exports):
11fdf7f2 487 export = Export.from_dict(1, {
f91f0fd5 488 'daemons': expected_exports[1],
11fdf7f2
TL
489 'export_id': 1,
490 'path': '/',
f91f0fd5 491 'cluster_id': cluster_id,
11fdf7f2
TL
492 'pseudo': '/cephfs_a',
493 'tag': None,
494 'access_type': 'RW',
495 'squash': 'root_squash',
496 'security_label': True,
497 'protocols': [4],
498 'transports': ['TCP', 'UDP'],
499 'clients': [{
500 'addresses': ["192.168.0.10", "192.168.1.0/8"],
501 'access_type': None,
502 'squash': 'no_root_squash'
503 }, {
504 'addresses': ["192.168.0.0/16"],
505 'access_type': 'RO',
506 'squash': 'all_squash'
507 }],
508 'fsal': {
509 'name': 'CEPH',
510 'user_id': 'ganesha',
511 'fs_name': 'a',
512 'sec_label_xattr': 'security.selinux'
513 }
514 })
515
516 self.assertEqual(export.export_id, 1)
517 self.assertEqual(export.path, "/")
518 self.assertEqual(export.pseudo, "/cephfs_a")
519 self.assertIsNone(export.tag)
520 self.assertEqual(export.access_type, "RW")
521 self.assertEqual(export.squash, "root_squash")
522 self.assertEqual(export.protocols, {4})
523 self.assertEqual(export.transports, {"TCP", "UDP"})
524 self.assertEqual(export.fsal.name, "CEPH")
525 self.assertEqual(export.fsal.user_id, "ganesha")
526 self.assertEqual(export.fsal.fs_name, "a")
527 self.assertEqual(export.fsal.sec_label_xattr, 'security.selinux')
528 self.assertEqual(len(export.clients), 2)
529 self.assertEqual(export.clients[0].addresses,
530 ["192.168.0.10", "192.168.1.0/8"])
531 self.assertEqual(export.clients[0].squash, "no_root_squash")
532 self.assertIsNone(export.clients[0].access_type)
533 self.assertEqual(export.clients[1].addresses, ["192.168.0.0/16"])
534 self.assertEqual(export.clients[1].squash, "all_squash")
535 self.assertEqual(export.clients[1].access_type, "RO")
f91f0fd5
TL
536 self.assertEqual(export.daemons, set(expected_exports[1]))
537 self.assertEqual(export.cluster_id, cluster_id)
11fdf7f2
TL
538 self.assertEqual(export.attr_expiration_time, 0)
539 self.assertEqual(export.security_label, True)
540
541 export = Export.from_dict(2, {
f91f0fd5 542 'daemons': expected_exports[2],
11fdf7f2
TL
543 'export_id': 2,
544 'path': '/',
545 'pseudo': '/rgw',
f91f0fd5 546 'cluster_id': cluster_id,
11fdf7f2
TL
547 'tag': None,
548 'access_type': 'RW',
549 'squash': 'all_squash',
550 'security_label': False,
551 'protocols': [4, 3],
552 'transports': ['TCP', 'UDP'],
553 'clients': [],
554 'fsal': {
555 'name': 'RGW',
556 'rgw_user_id': 'testuser'
557 }
558 })
559
560 self.assertEqual(export.export_id, 2)
561 self.assertEqual(export.path, "/")
562 self.assertEqual(export.pseudo, "/rgw")
563 self.assertIsNone(export.tag)
564 self.assertEqual(export.access_type, "RW")
565 self.assertEqual(export.squash, "all_squash")
566 self.assertEqual(export.protocols, {4, 3})
567 self.assertEqual(export.transports, {"TCP", "UDP"})
568 self.assertEqual(export.fsal.name, "RGW")
569 self.assertEqual(export.fsal.rgw_user_id, "testuser")
570 self.assertIsNone(export.fsal.access_key)
571 self.assertIsNone(export.fsal.secret_key)
572 self.assertEqual(len(export.clients), 0)
f91f0fd5
TL
573 self.assertEqual(export.daemons, set(expected_exports[2]))
574 self.assertEqual(export.cluster_id, cluster_id)
11fdf7f2
TL
575
576 def test_gen_raw_config(self):
f91f0fd5
TL
577 for cluster_id, info in self.clusters.items():
578 self._do_test_gen_raw_config(cluster_id, info['exports'])
579 self._reset_temp_store()
580
581 def _do_test_gen_raw_config(self, cluster_id, expected_exports):
582 conf = GaneshaConf.instance(cluster_id)
11fdf7f2
TL
583 # pylint: disable=W0212
584 export = conf.exports[1]
585 del conf.exports[1]
586 conf._save_export(export)
f91f0fd5 587 conf = GaneshaConf.instance(cluster_id)
11fdf7f2
TL
588 exports = conf.exports
589 self.assertEqual(len(exports.items()), 2)
590 self.assertIn(1, exports)
591 self.assertIn(2, exports)
592
593 # export_id = 1 asserts
594 export = exports[1]
595 self.assertEqual(export.export_id, 1)
596 self.assertEqual(export.path, "/")
597 self.assertEqual(export.pseudo, "/cephfs_a")
598 self.assertIsNone(export.tag)
599 self.assertEqual(export.access_type, "RW")
600 self.assertEqual(export.squash, "root_squash")
601 self.assertEqual(export.protocols, {4})
602 self.assertEqual(export.transports, {"TCP", "UDP"})
603 self.assertEqual(export.fsal.name, "CEPH")
604 self.assertEqual(export.fsal.user_id, "ganesha")
605 self.assertEqual(export.fsal.fs_name, "a")
606 self.assertEqual(export.fsal.sec_label_xattr, None)
607 self.assertEqual(len(export.clients), 2)
608 self.assertEqual(export.clients[0].addresses,
609 ["192.168.0.10", "192.168.1.0/8"])
610 self.assertEqual(export.clients[0].squash, "no_root_squash")
611 self.assertIsNone(export.clients[0].access_type)
612 self.assertEqual(export.clients[1].addresses, ["192.168.0.0/16"])
613 self.assertEqual(export.clients[1].squash, "all_squash")
614 self.assertEqual(export.clients[1].access_type, "RO")
f91f0fd5
TL
615 self.assertEqual(export.daemons, set(expected_exports[1]))
616 self.assertEqual(export.cluster_id, cluster_id)
11fdf7f2
TL
617 self.assertEqual(export.attr_expiration_time, 0)
618 self.assertEqual(export.security_label, False)
619
620 # export_id = 2 asserts
621 export = exports[2]
622 self.assertEqual(export.export_id, 2)
623 self.assertEqual(export.path, "/")
624 self.assertEqual(export.pseudo, "/rgw")
625 self.assertIsNone(export.tag)
626 self.assertEqual(export.access_type, "RW")
627 self.assertEqual(export.squash, "all_squash")
628 self.assertEqual(export.protocols, {4, 3})
629 self.assertEqual(export.transports, {"TCP", "UDP"})
630 self.assertEqual(export.fsal.name, "RGW")
631 self.assertEqual(export.fsal.rgw_user_id, "testuser")
632 self.assertEqual(export.fsal.access_key, "access_key")
633 self.assertEqual(export.fsal.secret_key, "secret_key")
634 self.assertEqual(len(export.clients), 0)
f91f0fd5
TL
635 self.assertEqual(export.daemons, set(expected_exports[2]))
636 self.assertEqual(export.cluster_id, cluster_id)
11fdf7f2
TL
637
638 def test_update_export(self):
f91f0fd5
TL
639 for cluster_id, info in self.clusters.items():
640 self._do_test_update_export(cluster_id, info['exports'])
641 self._reset_temp_store()
642
643 def _do_test_update_export(self, cluster_id, expected_exports):
11fdf7f2
TL
644 ganesha.RgwClient = MagicMock()
645 admin_inst_mock = MagicMock()
646 admin_inst_mock.get_user_keys.return_value = {
647 'access_key': 'access_key',
648 'secret_key': 'secret_key'
649 }
650 ganesha.RgwClient.admin_instance.return_value = admin_inst_mock
651
f91f0fd5 652 conf = GaneshaConf.instance(cluster_id)
11fdf7f2
TL
653 conf.update_export({
654 'export_id': 2,
f91f0fd5 655 'daemons': expected_exports[2],
11fdf7f2
TL
656 'path': 'bucket',
657 'pseudo': '/rgw/bucket',
f91f0fd5 658 'cluster_id': cluster_id,
11fdf7f2
TL
659 'tag': 'bucket_tag',
660 'access_type': 'RW',
661 'squash': 'all_squash',
662 'security_label': False,
663 'protocols': [4, 3],
664 'transports': ['TCP', 'UDP'],
665 'clients': [{
666 'addresses': ["192.168.0.0/16"],
667 'access_type': None,
668 'squash': None
669 }],
670 'fsal': {
671 'name': 'RGW',
672 'rgw_user_id': 'testuser'
673 }
674 })
675
f91f0fd5 676 conf = GaneshaConf.instance(cluster_id)
11fdf7f2
TL
677 export = conf.get_export(2)
678 self.assertEqual(export.export_id, 2)
679 self.assertEqual(export.path, "bucket")
680 self.assertEqual(export.pseudo, "/rgw/bucket")
681 self.assertEqual(export.tag, "bucket_tag")
682 self.assertEqual(export.access_type, "RW")
683 self.assertEqual(export.squash, "all_squash")
684 self.assertEqual(export.protocols, {4, 3})
685 self.assertEqual(export.transports, {"TCP", "UDP"})
686 self.assertEqual(export.fsal.name, "RGW")
687 self.assertEqual(export.fsal.rgw_user_id, "testuser")
688 self.assertEqual(export.fsal.access_key, "access_key")
689 self.assertEqual(export.fsal.secret_key, "secret_key")
690 self.assertEqual(len(export.clients), 1)
691 self.assertEqual(export.clients[0].addresses, ["192.168.0.0/16"])
692 self.assertIsNone(export.clients[0].squash)
693 self.assertIsNone(export.clients[0].access_type)
f91f0fd5
TL
694 self.assertEqual(export.daemons, set(expected_exports[2]))
695 self.assertEqual(export.cluster_id, cluster_id)
11fdf7f2
TL
696
697 def test_remove_export(self):
f91f0fd5
TL
698 for cluster_id, info in self.clusters.items():
699 self._do_test_remove_export(cluster_id, info['exports'])
700 self._reset_temp_store()
701
702 def _do_test_remove_export(self, cluster_id, expected_exports):
703 conf = GaneshaConf.instance(cluster_id)
11fdf7f2
TL
704 conf.remove_export(1)
705 exports = conf.list_exports()
706 self.assertEqual(len(exports), 1)
707 self.assertEqual(2, exports[0].export_id)
708 export = conf.get_export(2)
709 self.assertEqual(export.export_id, 2)
710 self.assertEqual(export.path, "/")
711 self.assertEqual(export.pseudo, "/rgw")
712 self.assertIsNone(export.tag)
713 self.assertEqual(export.access_type, "RW")
714 self.assertEqual(export.squash, "all_squash")
715 self.assertEqual(export.protocols, {4, 3})
716 self.assertEqual(export.transports, {"TCP", "UDP"})
717 self.assertEqual(export.fsal.name, "RGW")
718 self.assertEqual(export.fsal.rgw_user_id, "testuser")
719 self.assertEqual(export.fsal.access_key, "access_key")
720 self.assertEqual(export.fsal.secret_key, "secret_key")
721 self.assertEqual(len(export.clients), 0)
f91f0fd5
TL
722 self.assertEqual(export.daemons, set(expected_exports[2]))
723 self.assertEqual(export.cluster_id, cluster_id)
11fdf7f2
TL
724
725 def test_create_export_rgw(self):
f91f0fd5
TL
726 for cluster_id, info in self.clusters.items():
727 self._do_test_create_export_rgw(cluster_id, info['exports'])
728 self._reset_temp_store()
729
730 def _do_test_create_export_rgw(self, cluster_id, expected_exports):
11fdf7f2
TL
731 ganesha.RgwClient = MagicMock()
732 admin_inst_mock = MagicMock()
733 admin_inst_mock.get_user_keys.return_value = {
734 'access_key': 'access_key2',
735 'secret_key': 'secret_key2'
736 }
737 ganesha.RgwClient.admin_instance.return_value = admin_inst_mock
738
f91f0fd5 739 conf = GaneshaConf.instance(cluster_id)
11fdf7f2 740 ex_id = conf.create_export({
f91f0fd5 741 'daemons': expected_exports[3],
11fdf7f2
TL
742 'path': 'bucket',
743 'pseudo': '/rgw/bucket',
744 'tag': 'bucket_tag',
f91f0fd5 745 'cluster_id': cluster_id,
11fdf7f2
TL
746 'access_type': 'RW',
747 'squash': 'all_squash',
748 'security_label': False,
749 'protocols': [4, 3],
750 'transports': ['TCP', 'UDP'],
751 'clients': [{
752 'addresses': ["192.168.0.0/16"],
753 'access_type': None,
754 'squash': None
755 }],
756 'fsal': {
757 'name': 'RGW',
758 'rgw_user_id': 'testuser'
759 }
760 })
761
f91f0fd5 762 conf = GaneshaConf.instance(cluster_id)
11fdf7f2
TL
763 exports = conf.list_exports()
764 self.assertEqual(len(exports), 3)
765 export = conf.get_export(ex_id)
766 self.assertEqual(export.export_id, ex_id)
767 self.assertEqual(export.path, "bucket")
768 self.assertEqual(export.pseudo, "/rgw/bucket")
769 self.assertEqual(export.tag, "bucket_tag")
770 self.assertEqual(export.access_type, "RW")
771 self.assertEqual(export.squash, "all_squash")
772 self.assertEqual(export.protocols, {4, 3})
773 self.assertEqual(export.transports, {"TCP", "UDP"})
774 self.assertEqual(export.fsal.name, "RGW")
775 self.assertEqual(export.fsal.rgw_user_id, "testuser")
776 self.assertEqual(export.fsal.access_key, "access_key2")
777 self.assertEqual(export.fsal.secret_key, "secret_key2")
778 self.assertEqual(len(export.clients), 1)
779 self.assertEqual(export.clients[0].addresses, ["192.168.0.0/16"])
780 self.assertIsNone(export.clients[0].squash)
781 self.assertIsNone(export.clients[0].access_type)
f91f0fd5
TL
782 self.assertEqual(export.daemons, set(expected_exports[3]))
783 self.assertEqual(export.cluster_id, cluster_id)
11fdf7f2
TL
784
785 def test_create_export_cephfs(self):
f91f0fd5
TL
786 for cluster_id, info in self.clusters.items():
787 self._do_test_create_export_cephfs(cluster_id, info['exports'])
788 self._reset_temp_store()
789
790 def _do_test_create_export_cephfs(self, cluster_id, expected_exports):
11fdf7f2
TL
791 ganesha.CephX = MagicMock()
792 ganesha.CephX.list_clients.return_value = ["fs"]
793 ganesha.CephX.get_client_key.return_value = "fs_key"
794
795 ganesha.CephFS = MagicMock()
796 ganesha.CephFS.dir_exists.return_value = True
797
f91f0fd5 798 conf = GaneshaConf.instance(cluster_id)
11fdf7f2 799 ex_id = conf.create_export({
f91f0fd5 800 'daemons': expected_exports[3],
11fdf7f2
TL
801 'path': '/',
802 'pseudo': '/cephfs2',
f91f0fd5 803 'cluster_id': cluster_id,
11fdf7f2
TL
804 'tag': None,
805 'access_type': 'RW',
806 'squash': 'all_squash',
807 'security_label': True,
808 'protocols': [4],
809 'transports': ['TCP'],
810 'clients': [],
811 'fsal': {
812 'name': 'CEPH',
813 'user_id': 'fs',
814 'fs_name': None,
815 'sec_label_xattr': 'security.selinux'
816 }
817 })
818
f91f0fd5 819 conf = GaneshaConf.instance(cluster_id)
11fdf7f2
TL
820 exports = conf.list_exports()
821 self.assertEqual(len(exports), 3)
822 export = conf.get_export(ex_id)
823 self.assertEqual(export.export_id, ex_id)
824 self.assertEqual(export.path, "/")
825 self.assertEqual(export.pseudo, "/cephfs2")
826 self.assertIsNone(export.tag)
827 self.assertEqual(export.access_type, "RW")
828 self.assertEqual(export.squash, "all_squash")
829 self.assertEqual(export.protocols, {4})
830 self.assertEqual(export.transports, {"TCP"})
831 self.assertEqual(export.fsal.name, "CEPH")
832 self.assertEqual(export.fsal.user_id, "fs")
833 self.assertEqual(export.fsal.cephx_key, "fs_key")
834 self.assertEqual(export.fsal.sec_label_xattr, "security.selinux")
835 self.assertIsNone(export.fsal.fs_name)
836 self.assertEqual(len(export.clients), 0)
f91f0fd5
TL
837 self.assertEqual(export.daemons, set(expected_exports[3]))
838 self.assertEqual(export.cluster_id, cluster_id)
11fdf7f2
TL
839 self.assertEqual(export.attr_expiration_time, 0)
840 self.assertEqual(export.security_label, True)
f91f0fd5
TL
841
842 def test_reload_daemons(self):
843 # Fail to import call in Python 3.8, see https://bugs.python.org/issue35753
844 mock_call = unittest.mock.call
845
846 # Orchestrator cluster: reload all daemon config objects.
847 conf = GaneshaConf.instance('foo')
848 calls = [mock_call(conf) for conf in conf.list_daemon_confs()]
849 for daemons in [[], ['a', 'b']]:
850 conf.reload_daemons(daemons)
851 self.io_mock.notify.assert_has_calls(calls)
852 self.io_mock.reset_mock()
853
854 # User-defined cluster: reload daemons in the parameter
f67539c2 855 self._set_user_defined_clusters_location()
f91f0fd5
TL
856 conf = GaneshaConf.instance('_default_')
857 calls = [mock_call('conf-{}'.format(daemon)) for daemon in ['nodea', 'nodeb']]
858 conf.reload_daemons(['nodea', 'nodeb'])
859 self.io_mock.notify.assert_has_calls(calls)
860
861 def test_list_daemons(self):
862 for cluster_id, info in self.clusters.items():
863 instance = GaneshaConf.instance(cluster_id)
864 daemons = instance.list_daemons()
865 for daemon in daemons:
866 self.assertEqual(daemon['cluster_id'], cluster_id)
867 self.assertEqual(daemon['cluster_type'], info['type'])
868 self.assertIn('daemon_id', daemon)
869 self.assertIn('status', daemon)
870 self.assertIn('status_desc', daemon)
871 self.assertEqual([daemon['daemon_id'] for daemon in daemons], info['daemons'])
872
873 def test_validate_orchestrator(self):
874 cluster_id = 'foo'
875 cluster_info = self.clusters[cluster_id]
876 instance = GaneshaConf.instance(cluster_id)
877 export = MagicMock()
878
879 # export can be linked to none or all daemons
880 export_daemons = [[], cluster_info['daemons']]
881 for daemons in export_daemons:
882 export.daemons = daemons
883 instance.validate(export)
884
885 # raise if linking to partial or non-exist daemons
886 export_daemons = [cluster_info['daemons'][:1], 'xxx']
887 for daemons in export_daemons:
888 with self.assertRaises(NFSException):
889 export.daemons = daemons
890 instance.validate(export)
891
892 def test_validate_user(self):
f67539c2 893 self._set_user_defined_clusters_location()
f91f0fd5 894 cluster_id = '_default_'
f91f0fd5
TL
895 instance = GaneshaConf.instance(cluster_id)
896 export = MagicMock()
897
898 # export can be linked to none, partial, or all daemons
f67539c2
TL
899 fake_daemons = ['nodea', 'nodeb']
900 export_daemons = [[], fake_daemons[:1], fake_daemons]
f91f0fd5
TL
901 for daemons in export_daemons:
902 export.daemons = daemons
903 instance.validate(export)
904
905 # raise if linking to non-exist daemons
906 export_daemons = ['xxx']
907 for daemons in export_daemons:
908 with self.assertRaises(NFSException):
909 export.daemons = daemons
910 instance.validate(export)
911
912 def _verify_locations(self, locations, cluster_ids):
913 for cluster_id in cluster_ids:
914 self.assertIn(cluster_id, locations)
915 cluster = locations.pop(cluster_id)
f67539c2 916 self.assertDictEqual(cluster, {key: cluster[key] for key in [
f91f0fd5
TL
917 'pool', 'namespace', 'type', 'daemon_conf']})
918 self.assertDictEqual(locations, {})
919
920 def test_get_cluster_locations(self):
921 # pylint: disable=protected-access
f91f0fd5 922
f67539c2 923 # There is only a Orchestrator cluster.
f91f0fd5 924 self._mock_orchestrator(True)
f91f0fd5
TL
925 locations = ganesha.Ganesha._get_clusters_locations()
926 self._verify_locations(locations, ['foo'])
927
928 # No cluster.
929 self._mock_orchestrator(False)
930 with self.assertRaises(NFSException):
931 ganesha.Ganesha._get_clusters_locations()
932
f67539c2
TL
933 # There is only a user-defined cluster.
934 self._set_user_defined_clusters_location()
935 self._mock_orchestrator(False)
936 locations = ganesha.Ganesha._get_clusters_locations()
937 self._verify_locations(locations, ['_default_'])
938
939 # There are both Orchestrator cluster and user-defined cluster.
940 self._set_user_defined_clusters_location()
941 self._mock_orchestrator(True)
942 locations = ganesha.Ganesha._get_clusters_locations()
943 self._verify_locations(locations, ['foo', '_default_'])
944
f91f0fd5
TL
945 def test_get_cluster_locations_conflict(self):
946 # pylint: disable=protected-access
f67539c2
TL
947
948 # Pool/namespace collision.
949 self._set_user_defined_clusters_location('ganesha2/ns2')
950 with self.assertRaises(NFSException) as ctx:
951 ganesha.Ganesha._get_clusters_locations()
952 self.assertIn('already in use', str(ctx.exception))
953
954 # Cluster name collision with orch. cluster.
955 self._set_user_defined_clusters_location('foo:ganesha/ns')
956 with self.assertRaises(NFSException) as ctx:
957 ganesha.Ganesha._get_clusters_locations()
958 self.assertIn('Detected a conflicting NFS-Ganesha cluster', str(ctx.exception))
959
960 # Cluster name collision with user-defined cluster.
961 self._set_user_defined_clusters_location('cluster1:ganesha/ns,cluster1:fake-pool/fake-ns')
962 with self.assertRaises(NFSException) as ctx:
f91f0fd5 963 ganesha.Ganesha._get_clusters_locations()
f67539c2 964 self.assertIn('Duplicate Ganesha cluster definition', str(ctx.exception))
f91f0fd5
TL
965
966
967class NFSGaneshaUiControllerTest(ControllerTestCase):
968 @classmethod
969 def setup_server(cls):
970 # pylint: disable=protected-access
971 NFSGaneshaUi._cp_config['tools.authenticate.on'] = False
972 cls.setup_controllers([NFSGaneshaUi])
973
974 @classmethod
975 def _create_ls_dir_url(cls, fs_name, query_params):
976 api_url = '/ui-api/nfs-ganesha/lsdir/{}'.format(fs_name)
977 if query_params is not None:
978 return '{}?{}'.format(api_url, urlencode(query_params))
979 return api_url
980
981 @patch('dashboard.controllers.nfsganesha.CephFS')
982 def test_lsdir(self, cephfs_class):
983 cephfs_class.return_value.ls_dir.return_value = [
984 {'path': '/foo'},
985 {'path': '/foo/bar'}
986 ]
987 mocked_ls_dir = cephfs_class.return_value.ls_dir
988
989 reqs = [
990 {
991 'params': None,
992 'cephfs_ls_dir_args': ['/', 1],
993 'path0': '/',
994 'status': 200
995 },
996 {
997 'params': {'root_dir': '/', 'depth': '1'},
998 'cephfs_ls_dir_args': ['/', 1],
999 'path0': '/',
1000 'status': 200
1001 },
1002 {
1003 'params': {'root_dir': '', 'depth': '1'},
1004 'cephfs_ls_dir_args': ['/', 1],
1005 'path0': '/',
1006 'status': 200
1007 },
1008 {
1009 'params': {'root_dir': '/foo', 'depth': '3'},
1010 'cephfs_ls_dir_args': ['/foo', 3],
1011 'path0': '/foo',
1012 'status': 200
1013 },
1014 {
1015 'params': {'root_dir': 'foo', 'depth': '6'},
1016 'cephfs_ls_dir_args': ['/foo', 5],
1017 'path0': '/foo',
1018 'status': 200
1019 },
1020 {
1021 'params': {'root_dir': '/', 'depth': '-1'},
1022 'status': 400
1023 },
1024 {
1025 'params': {'root_dir': '/', 'depth': 'abc'},
1026 'status': 400
1027 }
1028 ]
1029
1030 for req in reqs:
1031 self._get(self._create_ls_dir_url('a', req['params']))
1032 self.assertStatus(req['status'])
1033
1034 # Returned paths should contain root_dir as first element
1035 if req['status'] == 200:
1036 paths = self.json_body()['paths']
1037 self.assertEqual(paths[0], req['path0'])
1038 cephfs_class.assert_called_once_with('a')
1039
1040 # Check the arguments passed to `CephFS.ls_dir`.
1041 if req.get('cephfs_ls_dir_args'):
1042 mocked_ls_dir.assert_called_once_with(*req['cephfs_ls_dir_args'])
1043 else:
1044 mocked_ls_dir.assert_not_called()
1045 mocked_ls_dir.reset_mock()
1046 cephfs_class.reset_mock()
1047
1048 @patch('dashboard.controllers.nfsganesha.cephfs')
1049 @patch('dashboard.controllers.nfsganesha.CephFS')
1050 def test_lsdir_non_existed_dir(self, cephfs_class, cephfs):
1051 cephfs.ObjectNotFound = Exception
1052 cephfs.PermissionError = Exception
1053 cephfs_class.return_value.ls_dir.side_effect = cephfs.ObjectNotFound()
1054 self._get(self._create_ls_dir_url('a', {'root_dir': '/foo', 'depth': '3'}))
1055 cephfs_class.assert_called_once_with('a')
1056 cephfs_class.return_value.ls_dir.assert_called_once_with('/foo', 3)
1057 self.assertStatus(200)
1058 self.assertJsonBody({'paths': []})