]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: NFS exports: API + UI: integration with mgr/nfs; cleanups
authorAlfonso Martínez <almartin@redhat.com>
Thu, 26 Aug 2021 10:05:54 +0000 (12:05 +0200)
committerAlfonso Martínez <almartin@redhat.com>
Wed, 3 Nov 2021 11:46:45 +0000 (12:46 +0100)
mgr/dashboard: move NFS_GANESHA_SUPPORTED_FSALS to mgr_module.py

Importing from nfs module throws AttributeError because as a side effect the dashboard module is impersonating the nfs module.
https://gist.github.com/varshar16/61ac26426bbe5f5f562ebb14bcd0f548

mgr/dashboard: 'Create NFS export' form: list clusters from nfs module

mgr/dashboard: frontend+backend cleanups for NFS export

Removed all code and references related to daemons. UI cleanup and adopted unit-testing for
nfs-epxort create form for CEPHFS backend. Cleanup for export list/get/create/set/delete endpoints.

mgr/dashboard: rm set-ganesha ref + update docs

Remove existing set-ganesha-clusters-rados-pool-namespace references as
they are no longer required. Moreover, nfs doc in dashboard doc is
updated accordingly to the current nfs status.

mgr/dashboard: add nfs-export e2e test coverage

mgr/dashboard: 'Create NFS export' form: remove RGW user id field.

- Improve bucket typeahead behavior.
- Increase version for bucket list endpoint.
- Some refactoring.

mgr/dashboard: 'Create NFS export' form: allow RGW backend only when default realm is selected.

When RGW multisite is configured, the NFS module can only handle buckets in the default realm.

mgr/dashboard: 'Create service' form: fix NFS service creation.

After https://github.com/ceph/ceph/pull/42073, NFS pool and namespace are not customizable.

mgr/dashboard: 'Create NFS export' form: add bucket validation.

- Allow only existing buckets.
- Refactoring:
  - Moved bucket validator from bucket form to cd-validators.ts
  - Split bucket validator into 2: bucket name validator and bucket existence (that checks either existence or non-existence).

mgr/dashboard: 'Create NFS export' form: path validation refactor: allow only existing paths.

Fixes: https://tracker.ceph.com/issues/46493
Fixes: https://tracker.ceph.com/issues/51479
Signed-off-by: Alfonso Martínez <almartin@redhat.com>
Signed-off-by: Avan Thakkar <athakkar@redhat.com>
Signed-off-by: Pere Diaz Bou <pdiazbou@redhat.com>
(cherry picked from commit 58a6ab2147c34d5b3f14bf48f9b47b03bea8a672)

 Conflicts:
doc/mgr/dashboard.rst
  - Resolved conflicts.
src/pybind/mgr/dashboard/services/cephx.py
  - Deleted as in master.

59 files changed:
doc/mgr/dashboard.rst
qa/tasks/mgr/dashboard/test_ganesha.py [deleted file]
qa/tasks/mgr/dashboard/test_rgw.py
src/pybind/mgr/dashboard/ci/cephadm/bootstrap-cluster.sh
src/pybind/mgr/dashboard/controllers/nfsganesha.py
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/cypress/integration/block/images.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/block/mirroring.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/services.po.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/07-nfs-exports.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/nfs/nfs-export.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/pools/pools.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/pools/pools.po.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/models/nfs.fsal.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-cluster-type.enum.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-daemon.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/api-client.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/api-client.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/nfs.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/nfs.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-site.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/plugins/feature_toggles.py
src/pybind/mgr/dashboard/services/cephx.py [deleted file]
src/pybind/mgr/dashboard/services/rgw_client.py
src/pybind/mgr/dashboard/tests/test_ganesha.py
src/pybind/mgr/dashboard/tests/test_rgw.py
src/pybind/mgr/mgr_module.py
src/pybind/mgr/nfs/cluster.py
src/pybind/mgr/nfs/export.py
src/pybind/mgr/nfs/export_utils.py
src/pybind/mgr/nfs/module.py
src/pybind/mgr/nfs/tests/test_nfs.py
src/vstart.sh

index 3ac0e0333b0a98661a142ebaccbfb31b9ac64fe1..7acd0695e9737cabff32526888d0faad3999e1bf 100644 (file)
@@ -1179,97 +1179,8 @@ A log entry may look like this::
 NFS-Ganesha Management
 ----------------------
 
-Support for NFS-Ganesha Clusters Deployed by the Orchestrator
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-The Ceph Dashboard can be used to manage NFS-Ganesha clusters deployed by the
-Orchestrator and will detect them automatically. For more details
-on deploying NFS-Ganesha clusters with the Orchestrator, please see:
-
-- Cephadm backend: :ref:`orchestrator-cli-stateless-services`. Or particularly, see
-  :ref:`deploy-cephadm-nfs-ganesha`.
-- Rook backend: `Ceph NFS Gateway CRD <https://rook.github.io/docs/rook/master/ceph-nfs-crd.html>`_.
-
-Support for NFS-Ganesha Clusters Defined by the User
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. note::
-
-    This configuration only applies for user-defined clusters,
-    NOT for Orchestrator-deployed clusters.
-
-The Ceph Dashboard can manage `NFS Ganesha <https://nfs-ganesha.github.io/>`_ exports that use
-CephFS or RGW as their backstore.
-
-To enable this feature in Ceph Dashboard there are some assumptions that need
-to be met regarding the way NFS-Ganesha services are configured.
-
-The dashboard manages NFS-Ganesha config files stored in RADOS objects on the Ceph Cluster.
-NFS-Ganesha must store part of their configuration in the Ceph cluster.
-
-These configuration files follow the below conventions.
-Each export block must be stored in its own RADOS object named
-``export-<id>``, where ``<id>`` must match the ``Export_ID`` attribute of the
-export configuration. Then, for each NFS-Ganesha service daemon there should
-exist a RADOS object named ``conf-<daemon_id>``, where ``<daemon_id>`` is an
-arbitrary string that should uniquely identify the daemon instance (e.g., the
-hostname where the daemon is running).
-Each ``conf-<daemon_id>`` object contains the RADOS URLs to the exports that
-the NFS-Ganesha daemon should serve. These URLs are of the form::
-
-  %url rados://<pool_name>[/<namespace>]/export-<id>
-
-Both the ``conf-<daemon_id>`` and ``export-<id>`` objects must be stored in the
-same RADOS pool/namespace.
-
-
-Configuring NFS-Ganesha in the Dashboard
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-To enable management of NFS-Ganesha exports in the Ceph Dashboard, we
-need to tell the Dashboard the RADOS pool and namespace in which
-configuration objects are stored. The Ceph Dashboard can then access them
-by following the naming convention described above.
-
-The Dashboard command to configure the NFS-Ganesha configuration objects
-location is::
-
-  $ ceph dashboard set-ganesha-clusters-rados-pool-namespace <pool_name>[/<namespace>]
-
-After running the above command, the Ceph Dashboard is able to find the NFS-Ganesha
-configuration objects and we can manage exports through the Web UI.
-
-.. note::
-
-    A dedicated pool for the NFS shares should be used. Otherwise it can cause the
-    `known issue <https://tracker.ceph.com/issues/46176>`_ with listing of shares
-    if the NFS objects are stored together with a lot of other objects in a single
-    pool.
-
-
-Support for Multiple NFS-Ganesha Clusters
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-The Ceph Dashboard also supports management of NFS-Ganesha exports belonging
-to other NFS-Ganesha clusters. An NFS-Ganesha cluster is a group of
-NFS-Ganesha service daemons sharing the same exports. NFS-Ganesha
-clusters are independent and don't share the exports configuration among each
-other.
-
-Each NFS-Ganesha cluster should store its configuration objects in a
-unique RADOS pool/namespace to isolate the configuration.
-
-To specify the the configuration location of each NFS-Ganesha cluster we
-can use the same command as above but with a different value pattern::
-
-  $ ceph dashboard set-ganesha-clusters-rados-pool-namespace <cluster_id>:<pool_name>[/<namespace>](,<cluster_id>:<pool_name>[/<namespace>])*
-
-The ``<cluster_id>`` is an arbitrary string that should uniquely identify the
-NFS-Ganesha cluster.
-
-When configuring the Ceph Dashboard with multiple NFS-Ganesha clusters, the
-Web UI will allow you to choose to which cluster an export belongs.
-
+The dashboard requires enabling the NFS module which will be used to manage
+NFS clusters and NFS exports. For more information check :ref:`mgr-nfs`.
 
 Plug-ins
 --------
diff --git a/qa/tasks/mgr/dashboard/test_ganesha.py b/qa/tasks/mgr/dashboard/test_ganesha.py
deleted file mode 100644 (file)
index 6868e0c..0000000
+++ /dev/null
@@ -1,208 +0,0 @@
-# -*- coding: utf-8 -*-
-# pylint: disable=too-many-public-methods
-
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, JList, JObj
-
-
-class GaneshaTest(DashboardTestCase):
-    CEPHFS = True
-    AUTH_ROLES = ['pool-manager', 'ganesha-manager']
-
-    @classmethod
-    def setUpClass(cls):
-        super(GaneshaTest, cls).setUpClass()
-        cls.create_pool('ganesha', 2**2, 'replicated')
-        cls._rados_cmd(['-p', 'ganesha', '-N', 'ganesha1', 'create', 'conf-node1'])
-        cls._rados_cmd(['-p', 'ganesha', '-N', 'ganesha1', 'create', 'conf-node2'])
-        cls._rados_cmd(['-p', 'ganesha', '-N', 'ganesha1', 'create', 'conf-node3'])
-        cls._rados_cmd(['-p', 'ganesha', '-N', 'ganesha2', 'create', 'conf-node1'])
-        cls._rados_cmd(['-p', 'ganesha', '-N', 'ganesha2', 'create', 'conf-node2'])
-        cls._rados_cmd(['-p', 'ganesha', '-N', 'ganesha2', 'create', 'conf-node3'])
-        cls._ceph_cmd(['dashboard', 'set-ganesha-clusters-rados-pool-namespace',
-                       'cluster1:ganesha/ganesha1,cluster2:ganesha/ganesha2'])
-
-        # RGW setup
-        cls._radosgw_admin_cmd([
-            'user', 'create', '--uid', 'admin', '--display-name', 'admin',
-            '--system', '--access-key', 'admin', '--secret', 'admin'
-        ])
-        cls._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-secret-key'], 'admin')
-        cls._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-access-key'], 'admin')
-
-    @classmethod
-    def tearDownClass(cls):
-        super(GaneshaTest, cls).tearDownClass()
-        cls._radosgw_admin_cmd(['user', 'rm', '--uid', 'admin', '--purge-data'])
-        cls._ceph_cmd(['osd', 'pool', 'delete', 'ganesha', 'ganesha',
-                       '--yes-i-really-really-mean-it'])
-
-    @DashboardTestCase.RunAs('test', 'test', [{'rbd-image': ['create', 'update', 'delete']}])
-    def test_read_access_permissions(self):
-        self._get('/api/nfs-ganesha/export')
-        self.assertStatus(403)
-
-    def test_list_daemons(self):
-        daemons = self._get("/api/nfs-ganesha/daemon")
-        self.assertEqual(len(daemons), 6)
-        daemons = [(d['daemon_id'], d['cluster_id']) for d in daemons]
-        self.assertIn(('node1', 'cluster1'), daemons)
-        self.assertIn(('node2', 'cluster1'), daemons)
-        self.assertIn(('node3', 'cluster1'), daemons)
-        self.assertIn(('node1', 'cluster2'), daemons)
-        self.assertIn(('node2', 'cluster2'), daemons)
-        self.assertIn(('node3', 'cluster2'), daemons)
-
-    @classmethod
-    def create_export(cls, path, cluster_id, daemons, fsal, sec_label_xattr=None):
-        if fsal == 'CEPH':
-            fsal = {"name": "CEPH", "user_id": "admin", "fs_name": None,
-                    "sec_label_xattr": sec_label_xattr}
-            pseudo = "/cephfs{}".format(path)
-        else:
-            fsal = {"name": "RGW", "rgw_user_id": "admin"}
-            pseudo = "/rgw/{}".format(path if path[0] != '/' else "")
-        ex_json = {
-            "path": path,
-            "fsal": fsal,
-            "cluster_id": cluster_id,
-            "daemons": daemons,
-            "pseudo": pseudo,
-            "tag": None,
-            "access_type": "RW",
-            "squash": "no_root_squash",
-            "security_label": sec_label_xattr is not None,
-            "protocols": [4],
-            "transports": ["TCP"],
-            "clients": [{
-                "addresses": ["10.0.0.0/8"],
-                "access_type": "RO",
-                "squash": "root"
-            }]
-        }
-        return cls._task_post('/api/nfs-ganesha/export', ex_json)
-
-    def tearDown(self):
-        super(GaneshaTest, self).tearDown()
-        exports = self._get("/api/nfs-ganesha/export")
-        if self._resp.status_code != 200:
-            return
-        self.assertIsInstance(exports, list)
-        for exp in exports:
-            self._task_delete("/api/nfs-ganesha/export/{}/{}"
-                              .format(exp['cluster_id'], exp['export_id']))
-
-    def _test_create_export(self, cephfs_path):
-        exports = self._get("/api/nfs-ganesha/export")
-        self.assertEqual(len(exports), 0)
-
-        data = self.create_export(cephfs_path, 'cluster1', ['node1', 'node2'], 'CEPH',
-                                  "security.selinux")
-
-        exports = self._get("/api/nfs-ganesha/export")
-        self.assertEqual(len(exports), 1)
-        self.assertDictEqual(exports[0], data)
-        return data
-
-    def test_create_export(self):
-        self._test_create_export('/foo')
-
-    def test_create_export_for_cephfs_root(self):
-        self._test_create_export('/')
-
-    def test_update_export(self):
-        export = self._test_create_export('/foo')
-        export['access_type'] = 'RO'
-        export['daemons'] = ['node1', 'node3']
-        export['security_label'] = True
-        data = self._task_put('/api/nfs-ganesha/export/{}/{}'
-                              .format(export['cluster_id'], export['export_id']),
-                              export)
-        exports = self._get("/api/nfs-ganesha/export")
-        self.assertEqual(len(exports), 1)
-        self.assertDictEqual(exports[0], data)
-        self.assertEqual(exports[0]['daemons'], ['node1', 'node3'])
-        self.assertEqual(exports[0]['security_label'], True)
-
-    def test_delete_export(self):
-        export = self._test_create_export('/foo')
-        self._task_delete("/api/nfs-ganesha/export/{}/{}"
-                          .format(export['cluster_id'], export['export_id']))
-        self.assertStatus(204)
-
-    def test_get_export(self):
-        exports = self._get("/api/nfs-ganesha/export")
-        self.assertEqual(len(exports), 0)
-
-        data1 = self.create_export("/foo", 'cluster2', ['node1', 'node2'], 'CEPH')
-        data2 = self.create_export("mybucket", 'cluster2', ['node2', 'node3'], 'RGW')
-
-        export1 = self._get("/api/nfs-ganesha/export/cluster2/1")
-        self.assertDictEqual(export1, data1)
-
-        export2 = self._get("/api/nfs-ganesha/export/cluster2/2")
-        self.assertDictEqual(export2, data2)
-
-    def test_invalid_status(self):
-        self._ceph_cmd(['dashboard', 'set-ganesha-clusters-rados-pool-namespace', ''])
-
-        data = self._get('/api/nfs-ganesha/status')
-        self.assertStatus(200)
-        self.assertIn('available', data)
-        self.assertIn('message', data)
-        self.assertFalse(data['available'])
-        self.assertIn(("NFS-Ganesha cluster is not detected. "
-                       "Please set the GANESHA_RADOS_POOL_NAMESPACE "
-                       "setting or deploy an NFS-Ganesha cluster with the Orchestrator."),
-                      data['message'])
-
-        self._ceph_cmd(['dashboard', 'set-ganesha-clusters-rados-pool-namespace',
-                        'cluster1:ganesha/ganesha1,cluster2:ganesha/ganesha2'])
-
-    def test_valid_status(self):
-        data = self._get('/api/nfs-ganesha/status')
-        self.assertStatus(200)
-        self.assertIn('available', data)
-        self.assertIn('message', data)
-        self.assertTrue(data['available'])
-
-    def test_ganesha_fsals(self):
-        data = self._get('/ui-api/nfs-ganesha/fsals')
-        self.assertStatus(200)
-        self.assertIn('CEPH', data)
-
-    def test_ganesha_filesystems(self):
-        data = self._get('/ui-api/nfs-ganesha/cephfs/filesystems')
-        self.assertStatus(200)
-        self.assertSchema(data, JList(JObj({
-            'id': int,
-            'name': str
-        })))
-
-    def test_ganesha_lsdir(self):
-        fss = self._get('/ui-api/nfs-ganesha/cephfs/filesystems')
-        self.assertStatus(200)
-        for fs in fss:
-            data = self._get('/ui-api/nfs-ganesha/lsdir/{}'.format(fs['name']))
-            self.assertStatus(200)
-            self.assertSchema(data, JObj({'paths': JList(str)}))
-            self.assertEqual(data['paths'][0], '/')
-
-    def test_ganesha_buckets(self):
-        data = self._get('/ui-api/nfs-ganesha/rgw/buckets')
-        self.assertStatus(200)
-        schema = JList(str)
-        self.assertSchema(data, schema)
-
-    def test_ganesha_clusters(self):
-        data = self._get('/ui-api/nfs-ganesha/clusters')
-        self.assertStatus(200)
-        schema = JList(str)
-        self.assertSchema(data, schema)
-
-    def test_ganesha_cephx_clients(self):
-        data = self._get('/ui-api/nfs-ganesha/cephx/clients')
-        self.assertStatus(200)
-        schema = JList(str)
-        self.assertSchema(data, schema)
index 1bfb995065968c866d06f3b94bf09d59f530ef3d..dc972d3ed0a4158f508c757fa68d179d5066f5a2 100644 (file)
@@ -183,13 +183,13 @@ class RgwBucketTest(RgwTestCase):
         self.assertEqual(data['tenant'], '')
 
         # List all buckets.
-        data = self._get('/api/rgw/bucket')
+        data = self._get('/api/rgw/bucket', version='1.1')
         self.assertStatus(200)
         self.assertEqual(len(data), 1)
         self.assertIn('teuth-test-bucket', data)
 
         # List all buckets with stats.
-        data = self._get('/api/rgw/bucket?stats=true')
+        data = self._get('/api/rgw/bucket?stats=true', version='1.1')
         self.assertStatus(200)
         self.assertEqual(len(data), 1)
         self.assertSchema(data[0], JObj(sub_elems={
@@ -203,7 +203,7 @@ class RgwBucketTest(RgwTestCase):
         }, allow_unknown=True))
 
         # List all buckets names without stats.
-        data = self._get('/api/rgw/bucket?stats=false')
+        data = self._get('/api/rgw/bucket?stats=false', version='1.1')
         self.assertStatus(200)
         self.assertEqual(data, ['teuth-test-bucket'])
 
@@ -283,7 +283,7 @@ class RgwBucketTest(RgwTestCase):
         # Delete the bucket.
         self._delete('/api/rgw/bucket/teuth-test-bucket')
         self.assertStatus(204)
-        data = self._get('/api/rgw/bucket')
+        data = self._get('/api/rgw/bucket', version='1.1')
         self.assertStatus(200)
         self.assertEqual(len(data), 0)
 
@@ -306,7 +306,7 @@ class RgwBucketTest(RgwTestCase):
         self.assertIsNone(data)
 
         # List all buckets.
-        data = self._get('/api/rgw/bucket')
+        data = self._get('/api/rgw/bucket', version='1.1')
         self.assertStatus(200)
         self.assertEqual(len(data), 1)
         self.assertIn('testx/teuth-test-bucket', data)
@@ -379,7 +379,7 @@ class RgwBucketTest(RgwTestCase):
         self._delete('/api/rgw/bucket/{}'.format(
             parse.quote_plus('testx/teuth-test-bucket')))
         self.assertStatus(204)
-        data = self._get('/api/rgw/bucket')
+        data = self._get('/api/rgw/bucket', version='1.1')
         self.assertStatus(200)
         self.assertEqual(len(data), 0)
 
index 10a060a9bceb6afe29eb667a3c5dd6e06b557fb2..2c451f7864c100e5186f625a44c3aca9a4dce4ac 100755 (executable)
@@ -11,14 +11,15 @@ mon_ip=$(ifconfig eth0  | grep 'inet ' | awk '{ print $2}')
 cephadm bootstrap --mon-ip $mon_ip --initial-dashboard-password {{ admin_password }} --allow-fqdn-hostname --skip-monitoring-stack --dashboard-password-noupdate --shared_ceph_folder /mnt/{{ ceph_dev_folder }}
 
 fsid=$(cat /etc/ceph/ceph.conf | grep fsid | awk '{ print $3}')
+cephadm_shell="cephadm shell --fsid ${fsid} -c /etc/ceph/ceph.conf -k /etc/ceph/ceph.client.admin.keyring"
 
 {% for number in range(1, nodes) %}
   ssh-copy-id -f -i /etc/ceph/ceph.pub  -o StrictHostKeyChecking=no root@{{ prefix }}-node-0{{ number }}.{{ domain }}
   {% if expanded_cluster is defined %}
-    cephadm shell --fsid $fsid -c /etc/ceph/ceph.conf -k /etc/ceph/ceph.client.admin.keyring ceph orch host add {{ prefix }}-node-0{{ number }}.{{ domain }}
+    ${cephadm_shell} ceph orch host add {{ prefix }}-node-0{{ number }}.{{ domain }}
   {% endif %}
 {% endfor %}
 
 {% if expanded_cluster is defined %}
-  cephadm shell --fsid $fsid -c /etc/ceph/ceph.conf -k /etc/ceph/ceph.client.admin.keyring ceph orch apply osd --all-available-devices
+  ${cephadm_shell} ceph orch apply osd --all-available-devices
 {% endif %}
index 1f5d738cf252d1b12daad3b9b3cd7ce5b7b2abd1..7d16b91432fb1c2cd603ec0c76ee175324682a4f 100644 (file)
@@ -1,26 +1,23 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import
 
+import json
 import logging
 import os
-import json
 from functools import partial
+from typing import Any, Dict, List, Optional
 
 import cephfs
 import cherrypy
-# Importing from nfs module throws Attribute Error
-# https://gist.github.com/varshar16/61ac26426bbe5f5f562ebb14bcd0f548
-#from nfs.export_utils import NFS_GANESHA_SUPPORTED_FSALS
-#from nfs.utils import available_clusters
+from mgr_module import NFS_GANESHA_SUPPORTED_FSALS
 
 from .. import mgr
 from ..security import Scope
 from ..services.cephfs import CephFS
 from ..services.exception import DashboardException, serialize_dashboard_exception
-from ..services.rgw_client import NoCredentialsException, \
-    NoRgwDaemonsException, RequestException, RgwClient
 from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \
     ReadPermission, RESTController, Task, UIRouter
+from ._version import APIVersion
 
 logger = logging.getLogger('controllers.nfs')
 
@@ -29,15 +26,12 @@ class NFSException(DashboardException):
     def __init__(self, msg):
         super(NFSException, self).__init__(component="nfs", msg=msg)
 
-# Remove this once attribute error is fixed
-NFS_GANESHA_SUPPORTED_FSALS = ['CEPH', 'RGW']
 
 # documentation helpers
 EXPORT_SCHEMA = {
     'export_id': (int, 'Export ID'),
     'path': (str, 'Export path'),
     'cluster_id': (str, 'Cluster identifier'),
-    'daemons': ([str], 'List of NFS Ganesha daemons identifiers'),
     'pseudo': (str, 'Pseudo FS path'),
     'access_type': (str, 'Export access type'),
     'squash': (str, 'Export squash policy'),
@@ -46,10 +40,9 @@ EXPORT_SCHEMA = {
     'transports': ([str], 'List of transport types'),
     'fsal': ({
         'name': (str, 'name of FSAL'),
-        'user_id': (str, 'CephX user id', True),
-        'filesystem': (str, 'CephFS filesystem ID', True),
+        'fs_name': (str, 'CephFS filesystem name', True),
         'sec_label_xattr': (str, 'Name of xattr for security label', True),
-        'rgw_user_id': (str, 'RGW user id', True)
+        'user_id': (str, 'User id', True)
     }, 'FSAL configuration'),
     'clients': ([{
         'addresses': ([str], 'list of IP addresses'),
@@ -62,7 +55,6 @@ EXPORT_SCHEMA = {
 CREATE_EXPORT_SCHEMA = {
     'path': (str, 'Export path'),
     'cluster_id': (str, 'Cluster identifier'),
-    'daemons': ([str], 'List of NFS Ganesha daemons identifiers'),
     'pseudo': (str, 'Pseudo FS path'),
     'access_type': (str, 'Export access type'),
     'squash': (str, 'Export squash policy'),
@@ -71,19 +63,14 @@ CREATE_EXPORT_SCHEMA = {
     'transports': ([str], 'List of transport types'),
     'fsal': ({
         'name': (str, 'name of FSAL'),
-        'user_id': (str, 'CephX user id', True),
-        'filesystem': (str, 'CephFS filesystem ID', True),
-        'sec_label_xattr': (str, 'Name of xattr for security label', True),
-        'rgw_user_id': (str, 'RGW user id', True)
+        'fs_name': (str, 'CephFS filesystem name', True),
+        'sec_label_xattr': (str, 'Name of xattr for security label', True)
     }, 'FSAL configuration'),
     'clients': ([{
         'addresses': ([str], 'list of IP addresses'),
         'access_type': (str, 'Client access type'),
         'squash': (str, 'Client squash policy')
-    }], 'List of client configurations'),
-    'reload_daemons': (bool,
-                       'Trigger reload of NFS-Ganesha daemons configuration',
-                       True)
+    }], 'List of client configurations')
 }
 
 
@@ -97,7 +84,7 @@ def NfsTask(name, metadata, wait_for):  # noqa: N802
 
 
 @APIRouter('/nfs-ganesha', Scope.NFS_GANESHA)
-@APIDoc("NFS-Ganesha Management API", "NFS-Ganesha")
+@APIDoc("NFS-Ganesha Cluster Management API", "NFS-Ganesha")
 class NFSGanesha(RESTController):
 
     @EndpointDoc("Status of NFS-Ganesha management feature",
@@ -108,19 +95,24 @@ class NFSGanesha(RESTController):
     @Endpoint()
     @ReadPermission
     def status(self):
-        '''
-        FIXME: update this to check if any nfs cluster is available. Otherwise this endpoint can be safely removed too.
-        As it was introduced to check dashboard pool and namespace configuration.
+        status = {'available': True, 'message': None}
         try:
-            cluster_ls = available_clusters(mgr)
-            if not cluster_ls:
-                raise NFSException('Please deploy a cluster using `nfs cluster create ... or orch apply nfs ..')
-        except (NameError, ImportError) as e:
-            status['message'] = str(e)  # type: ignore
+            mgr.remote('nfs', 'cluster_ls')
+        except ImportError as error:
+            logger.exception(error)
             status['available'] = False
+            status['message'] = str(error)  # type: ignore
+
         return status
-        '''
-        return {'available': True, 'message': None}
+
+
+@APIRouter('/nfs-ganesha/cluster', Scope.NFS_GANESHA)
+@APIDoc(group="NFS-Ganesha")
+class NFSGaneshaCluster(RESTController):
+    @ReadPermission
+    @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
+    def list(self):
+        return mgr.remote('nfs', 'cluster_ls')
 
 
 @APIRouter('/nfs-ganesha/export', Scope.NFS_GANESHA)
@@ -128,33 +120,43 @@ class NFSGanesha(RESTController):
 class NFSGaneshaExports(RESTController):
     RESOURCE_ID = "cluster_id/export_id"
 
+    @staticmethod
+    def _get_schema_export(export: Dict[str, Any]) -> Dict[str, Any]:
+        """
+        Method that avoids returning export info not exposed in the export schema
+        e.g., rgw user access/secret keys.
+        """
+        schema_fsal_info = {}
+        for key in export['fsal'].keys():
+            if key in EXPORT_SCHEMA['fsal'][0].keys():  # type: ignore
+                schema_fsal_info[key] = export['fsal'][key]
+        export['fsal'] = schema_fsal_info
+        return export
+
     @EndpointDoc("List all NFS-Ganesha exports",
                  responses={200: [EXPORT_SCHEMA]})
-    def list(self):
-        '''
-        list exports based on cluster_id ?
-        export_mgr = mgr.remote('nfs', 'fetch_nfs_export_obj')
-        ret, out, err = export_mgr.list_exports(cluster_id=cluster_id, detailed=True)
-        if ret == 0:
-            return json.loads(out)
-        raise NFSException(f"Failed to list exports: {err}")
-        '''
-        return mgr.remote('nfs', 'export_ls')
+    def list(self) -> List[Dict[str, Any]]:
+        exports = []
+        for export in mgr.remote('nfs', 'export_ls'):
+            exports.append(self._get_schema_export(export))
+
+        return exports
 
     @NfsTask('create', {'path': '{path}', 'fsal': '{fsal.name}',
                         'cluster_id': '{cluster_id}'}, 2.0)
     @EndpointDoc("Creates a new NFS-Ganesha export",
                  parameters=CREATE_EXPORT_SCHEMA,
                  responses={201: EXPORT_SCHEMA})
-    def create(self, path, cluster_id, daemons, pseudo, access_type,
-               squash, security_label, protocols, transports, fsal, clients,
-               reload_daemons=True):
-        fsal.pop('user_id')  # mgr/nfs does not let you customize user_id
+    @RESTController.MethodMap(version=APIVersion(2, 0))  # type: ignore
+    def create(self, path, cluster_id, pseudo, access_type,
+               squash, security_label, protocols, transports, fsal, clients) -> Dict[str, Any]:
+
+        if hasattr(fsal, 'user_id'):
+            fsal.pop('user_id')  # mgr/nfs does not let you customize user_id
         raw_ex = {
             'path': path,
             'pseudo': pseudo,
             'cluster_id': cluster_id,
-            'daemons': daemons,
             'access_type': access_type,
             'squash': squash,
             'security_label': security_label,
@@ -164,28 +166,25 @@ class NFSGaneshaExports(RESTController):
             'clients': clients
         }
         export_mgr = mgr.remote('nfs', 'fetch_nfs_export_obj')
-        ret, out, err = export_mgr.apply_export(cluster_id, json.dumps(raw_ex))
+        ret, _, err = export_mgr.apply_export(cluster_id, json.dumps(raw_ex))
         if ret == 0:
-            return export_mgr._get_export_dict(cluster_id, pseudo)
+            return self._get_schema_export(
+                export_mgr._get_export_dict(cluster_id, pseudo))  # pylint: disable=W0212
         raise NFSException(f"Export creation failed {err}")
 
     @EndpointDoc("Get an NFS-Ganesha export",
                  parameters={
                      'cluster_id': (str, 'Cluster identifier'),
-                     'export_id': (int, "Export ID")
+                     'export_id': (str, "Export ID")
                  },
                  responses={200: EXPORT_SCHEMA})
-    def get(self, cluster_id, export_id):
-        '''
-         Get export by pseudo path?
-         export_mgr = mgr.remote('nfs', 'fetch_nfs_export_obj')
-        return export_mgr._get_export_dict(cluster_id, pseudo)
-
-         Get export by id
-         export_mgr = mgr.remote('nfs', 'fetch_nfs_export_obj')
-         return export_mgr.get_export_by_id(cluster_id, export_id)
-        '''
-        return mgr.remote('nfs', 'export_get', cluster_id, export_id)
+    def get(self, cluster_id, export_id) -> Optional[Dict[str, Any]]:
+        export_id = int(export_id)
+        export = mgr.remote('nfs', 'export_get', cluster_id, export_id)
+        if export:
+            export = self._get_schema_export(export)
+
+        return export
 
     @NfsTask('edit', {'cluster_id': '{cluster_id}', 'export_id': '{export_id}'},
              2.0)
@@ -193,16 +192,17 @@ class NFSGaneshaExports(RESTController):
                  parameters=dict(export_id=(int, "Export ID"),
                                  **CREATE_EXPORT_SCHEMA),
                  responses={200: EXPORT_SCHEMA})
-    def set(self, cluster_id, export_id, path, daemons, pseudo, access_type,
-            squash, security_label, protocols, transports, fsal, clients,
-            reload_daemons=True):
+    @RESTController.MethodMap(version=APIVersion(2, 0))  # type: ignore
+    def set(self, cluster_id, export_id, path, pseudo, access_type,
+            squash, security_label, protocols, transports, fsal, clients) -> Dict[str, Any]:
 
-        fsal.pop('user_id')  # mgr/nfs does not let you customize user_id
+        if hasattr(fsal, 'user_id'):
+            fsal.pop('user_id')  # mgr/nfs does not let you customize user_id
         raw_ex = {
             'path': path,
             'pseudo': pseudo,
             'cluster_id': cluster_id,
-            'daemons': daemons,
+            'export_id': export_id,
             'access_type': access_type,
             'squash': squash,
             'security_label': security_label,
@@ -213,9 +213,10 @@ class NFSGaneshaExports(RESTController):
         }
 
         export_mgr = mgr.remote('nfs', 'fetch_nfs_export_obj')
-        ret, out, err = export_mgr.apply_export(cluster_id, json.dumps(raw_ex))
+        ret, _, err = export_mgr.apply_export(cluster_id, json.dumps(raw_ex))
         if ret == 0:
-            return export_mgr._get_export_dict(cluster_id, pseudo)
+            return self._get_schema_export(
+                export_mgr._get_export_dict(cluster_id, pseudo))  # pylint: disable=W0212
         raise NFSException(f"Failed to update export: {err}")
 
     @NfsTask('delete', {'cluster_id': '{cluster_id}',
@@ -223,25 +224,10 @@ class NFSGaneshaExports(RESTController):
     @EndpointDoc("Deletes an NFS-Ganesha export",
                  parameters={
                      'cluster_id': (str, 'Cluster identifier'),
-                     'export_id': (int, "Export ID"),
-                     'reload_daemons': (bool,
-                                        'Trigger reload of NFS-Ganesha daemons'
-                                        ' configuration',
-                                        True)
+                     'export_id': (int, "Export ID")
                  })
-    def delete(self, cluster_id, export_id, reload_daemons=True):
-        '''
-         Delete by pseudo path
-         export_mgr = mgr.remote('nfs', 'fetch_nfs_export_obj')
-         export_mgr.delete_export(cluster_id, pseudo)
-
-         if deleting by export id
-         export_mgr = mgr.remote('nfs', 'fetch_nfs_export_obj')
-         export = export_mgr.get_export_by_id(cluster_id, export_id)
-         ret, out, err = export_mgr.delete_export(cluster_id=cluster_id, pseudo_path=export['pseudo'])
-         if ret != 0:
-            raise NFSException(err)
-        '''
+    @RESTController.MethodMap(version=APIVersion(2, 0))  # type: ignore
+    def delete(self, cluster_id, export_id):
         export_id = int(export_id)
 
         export = mgr.remote('nfs', 'export_get', cluster_id, export_id)
@@ -250,31 +236,8 @@ class NFSGaneshaExports(RESTController):
         mgr.remote('nfs', 'export_rm', cluster_id, export['pseudo'])
 
 
-# FIXME: remove this; dashboard should only care about clusters.
-@APIRouter('/nfs-ganesha/daemon', Scope.NFS_GANESHA)
-@APIDoc(group="NFS-Ganesha")
-class NFSGaneshaService(RESTController):
-
-    @EndpointDoc("List NFS-Ganesha daemons information",
-                 responses={200: [{
-                     'daemon_id': (str, 'Daemon identifier'),
-                     'cluster_id': (str, 'Cluster identifier'),
-                     'cluster_type': (str, 'Cluster type'),   # FIXME: remove this property
-                     'status': (int, 'Status of daemon', True),
-                     'desc': (str, 'Status description', True)
-                 }]})
-    def list(self):
-        return mgr.remote('nfs', 'daemon_ls')
-
-
 @UIRouter('/nfs-ganesha', Scope.NFS_GANESHA)
 class NFSGaneshaUi(BaseController):
-    @Endpoint('GET', '/cephx/clients')
-    @ReadPermission
-    def cephx_clients(self):
-        # FIXME: remove this; cephx users/creds are managed by mgr/nfs
-        return ['admin']
-
     @Endpoint('GET', '/fsals')
     @ReadPermission
     def fsals(self):
@@ -319,31 +282,3 @@ class NFSGaneshaUi(BaseController):
     @ReadPermission
     def filesystems(self):
         return CephFS.list_filesystems()
-
-    @Endpoint('GET', '/rgw/buckets')
-    @ReadPermission
-    def buckets(self, user_id=None):
-        try:
-            return RgwClient.instance(user_id).get_buckets()
-        except (DashboardException, NoCredentialsException, RequestException,
-                NoRgwDaemonsException):
-            return []
-
-    @Endpoint('GET', '/clusters')
-    @ReadPermission
-    def clusters(self):
-        '''
-        Remove this remote call instead directly use available_cluster() method. It returns list of cluster names: ['vstart']
-        The current dashboard api needs to changed from following to simply list of strings
-              [
-                {
-                     'pool': 'nfs-ganesha',
-                     'namespace': cluster_id,
-                     'type': 'orchestrator',
-                     'daemon_conf': None
-                 } for cluster_id in available_clusters()
-               ]
-        As pool, namespace, cluster type and daemon_conf are not required for listing cluster by mgr/nfs module
-        return available_cluster(mgr)
-        '''
-        return mgr.remote('nfs', 'cluster_ls')
index 32885dc537f186417d7f270f7100c11ff70b95e2..5f599b96c941689f3d0d45fcdae23d61e4d20644 100644 (file)
@@ -15,9 +15,10 @@ from ..services.rgw_client import NoRgwDaemonsException, RgwClient
 from ..tools import json_str_to_object, str_to_bool
 from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \
     ReadPermission, RESTController, allow_empty_body
+from ._version import APIVersion
 
 try:
-    from typing import Any, List, Optional
+    from typing import Any, Dict, List, Optional, Union
 except ImportError:  # pragma: no cover
     pass  # Just for type checking
 
@@ -101,6 +102,7 @@ class RgwDaemon(RESTController):
                     'service_map_id': service['id'],
                     'version': metadata['ceph_version'],
                     'server_hostname': hostname,
+                    'realm_name': metadata['realm_name'],
                     'zonegroup_name': metadata['zonegroup_name'],
                     'zone_name': metadata['zone_name'],
                     'default': instance.daemon.name == metadata['id']
@@ -158,6 +160,8 @@ class RgwSite(RgwRESTController):
             return RgwClient.admin_instance(daemon_name=daemon_name).get_placement_targets()
         if query == 'realms':
             return RgwClient.admin_instance(daemon_name=daemon_name).get_realms()
+        if query == 'default-realm':
+            return RgwClient.admin_instance(daemon_name=daemon_name).get_default_realm()
 
         # @TODO: for multisite: by default, retrieve cluster topology/map.
         raise DashboardException(http_status_code=501, component='rgw', msg='Not Implemented')
@@ -232,9 +236,12 @@ class RgwBucket(RgwRESTController):
             bucket_name = '{}:{}'.format(tenant, bucket_name)
         return bucket_name
 
-    def list(self, stats=False, daemon_name=None):
-        # type: (bool, Optional[str]) -> List[Any]
-        query_params = '?stats' if str_to_bool(stats) else ''
+    @RESTController.MethodMap(version=APIVersion(1, 1))  # type: ignore
+    def list(self, stats: bool = False, daemon_name: Optional[str] = None,
+             uid: Optional[str] = None) -> List[Union[str, Dict[str, Any]]]:
+        query_params = f'?stats={str_to_bool(stats)}'
+        if uid and uid.strip():
+            query_params = f'{query_params}&uid={uid.strip()}'
         result = self.proxy(daemon_name, 'GET', 'bucket{}'.format(query_params))
 
         if stats:
index cf8832bb9bbc07ac98d10d67821facf87847d350..5c89359db790e64ba0c22dd4099ec782feb5a328 100644 (file)
@@ -13,7 +13,7 @@ describe('Images page', () => {
     // Need pool for image testing
     pools.navigateTo('create');
     pools.create(poolName, 8, 'rbd');
-    pools.exist(poolName, true);
+    pools.existTableCell(poolName);
   });
 
   after(() => {
@@ -21,7 +21,7 @@ describe('Images page', () => {
     pools.navigateTo();
     pools.delete(poolName);
     pools.navigateTo();
-    pools.exist(poolName, false);
+    pools.existTableCell(poolName, false);
   });
 
   beforeEach(() => {
index ddee817e18ef1232baf21395347679455d739d63..120956579d8d188feccf9f8f0a10eb49acf725e2 100644 (file)
@@ -32,7 +32,7 @@ describe('Mirroring page', () => {
       pools.navigateTo('create'); // Need pool for mirroring testing
       pools.create(poolName, 8, 'rbd');
       pools.navigateTo();
-      pools.exist(poolName, true);
+      pools.existTableCell(poolName, true);
     });
 
     it('tests editing mode for pools', () => {
index 731275e26d1c15a2d3812423b7218be19a962fac..9868b89aedbc5489298b34355d7a092078a44a06 100644 (file)
@@ -45,7 +45,7 @@ describe('Logs page', () => {
       pools.navigateTo('create');
       pools.create(poolname, 8);
       pools.navigateTo();
-      pools.exist(poolname, true);
+      pools.existTableCell(poolname, true);
       logs.checkAuditForPoolFunction(poolname, 'create', hour, minute);
     });
 
index 4265329db042ab8754563d189476e57f9a2449d8..457b759ead39f61d9eb0863c9de527473658b91b 100644 (file)
@@ -40,15 +40,24 @@ export class ServicesPageHelper extends PageHelper {
   addService(serviceType: string, exist?: boolean, count = '1') {
     cy.get(`${this.pages.create.id}`).within(() => {
       this.selectServiceType(serviceType);
-      if (serviceType === 'rgw') {
-        cy.get('#service_id').type('foo');
-        cy.get('#count').type(count);
-      } else if (serviceType === 'ingress') {
-        this.selectOption('backend_service', 'rgw.foo');
-        cy.get('#service_id').should('have.value', 'rgw.foo');
-        cy.get('#virtual_ip').type('192.168.20.1/24');
-        cy.get('#frontend_port').type('8081');
-        cy.get('#monitor_port').type('8082');
+      switch (serviceType) {
+        case 'rgw':
+          cy.get('#service_id').type('foo');
+          cy.get('#count').type(count);
+          break;
+
+        case 'ingress':
+          this.selectOption('backend_service', 'rgw.foo');
+          cy.get('#service_id').should('have.value', 'rgw.foo');
+          cy.get('#virtual_ip').type('192.168.20.1/24');
+          cy.get('#frontend_port').type('8081');
+          cy.get('#monitor_port').type('8082');
+          break;
+
+        case 'nfs':
+          cy.get('#service_id').type('testnfs');
+          cy.get('#count').type(count);
+          break;
       }
 
       cy.get('cd-submit-button').click();
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/07-nfs-exports.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/07-nfs-exports.e2e-spec.ts
new file mode 100644 (file)
index 0000000..2d92075
--- /dev/null
@@ -0,0 +1,81 @@
+import { ServicesPageHelper } from 'cypress/integration/cluster/services.po';
+import { NFSPageHelper } from 'cypress/integration/orchestrator/workflow/nfs/nfs-export.po';
+import { BucketsPageHelper } from 'cypress/integration/rgw/buckets.po';
+
+describe('nfsExport page', () => {
+  const nfsExport = new NFSPageHelper();
+  const services = new ServicesPageHelper();
+  const buckets = new BucketsPageHelper();
+  const bucketName = 'e2e.nfs.bucket';
+  // @TODO: uncomment this when a CephFS volume can be created through Dashboard.
+  // const fsPseudo = '/fsPseudo';
+  const rgwPseudo = '/rgwPseudo';
+  const editPseudo = '/editPseudo';
+  const backends = ['CephFS', 'Object Gateway'];
+  const squash = 'no_root_squash';
+  const client: object = { addresses: '192.168.0.10' };
+
+  beforeEach(() => {
+    cy.login();
+    Cypress.Cookies.preserveOnce('token');
+    nfsExport.navigateTo();
+  });
+
+  describe('breadcrumb test', () => {
+    it('should open and show breadcrumb', () => {
+      nfsExport.expectBreadcrumbText('NFS');
+    });
+  });
+
+  describe('Create, edit and delete', () => {
+    it('should create an NFS cluster', () => {
+      services.navigateTo('create');
+
+      services.addService('nfs');
+
+      services.checkExist('nfs.testnfs', true);
+      services.getExpandCollapseElement().click();
+      services.checkServiceStatus('nfs');
+    });
+
+    it('should create a nfs-export with RGW backend', () => {
+      buckets.navigateTo('create');
+      buckets.create(bucketName, 'dashboard', 'default-placement');
+
+      nfsExport.navigateTo();
+      nfsExport.existTableCell(rgwPseudo, false);
+      nfsExport.navigateTo('create');
+      nfsExport.create(backends[1], squash, client, rgwPseudo, bucketName);
+      nfsExport.existTableCell(rgwPseudo);
+    });
+
+    // @TODO: uncomment this when a CephFS volume can be created through Dashboard.
+    // it('should create a nfs-export with CephFS backend', () => {
+    //   nfsExport.navigateTo();
+    //   nfsExport.existTableCell(fsPseudo, false);
+    //   nfsExport.navigateTo('create');
+    //   nfsExport.create(backends[0], squash, client, fsPseudo);
+    //   nfsExport.existTableCell(fsPseudo);
+    // });
+
+    it('should show Clients', () => {
+      nfsExport.clickTab('cd-nfs-details', rgwPseudo, 'Clients (1)');
+      cy.get('cd-nfs-details').within(() => {
+        nfsExport.getTableCount('total').should('be.gte', 0);
+      });
+    });
+
+    it('should edit an export', () => {
+      nfsExport.editExport(rgwPseudo, editPseudo);
+
+      nfsExport.existTableCell(editPseudo);
+    });
+
+    it('should delete exports and bucket', () => {
+      nfsExport.delete(editPseudo);
+
+      buckets.navigateTo();
+      buckets.delete(bucketName);
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/nfs/nfs-export.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/nfs/nfs-export.po.ts
new file mode 100644 (file)
index 0000000..91dfdf4
--- /dev/null
@@ -0,0 +1,45 @@
+import { PageHelper } from 'cypress/integration/page-helper.po';
+
+const pages = {
+  index: { url: '#/nfs', id: 'cd-nfs-list' },
+  create: { url: '#/nfs/create', id: 'cd-nfs-form' }
+};
+
+export class NFSPageHelper extends PageHelper {
+  pages = pages;
+
+  @PageHelper.restrictTo(pages.create.url)
+  create(backend: string, squash: string, client: object, pseudo: string, rgwPath?: string) {
+    this.selectOption('cluster_id', 'testnfs');
+    // select a storage backend
+    this.selectOption('name', backend);
+    if (backend === 'CephFS') {
+      this.selectOption('fs_name', 'myfs');
+
+      cy.get('#security_label').click({ force: true });
+    } else {
+      cy.get('input[data-testid=rgw_path]').type(rgwPath);
+    }
+
+    cy.get('input[name=pseudo]').type(pseudo);
+    this.selectOption('squash', squash);
+
+    // Add clients
+    cy.get('button[name=add_client]').click({ force: true });
+    cy.get('input[name=addresses]').type(client['addresses']);
+
+    cy.get('cd-submit-button').click();
+  }
+
+  editExport(pseudo: string, editPseudo: string) {
+    this.navigateEdit(pseudo);
+
+    cy.get('input[name=pseudo]').clear().type(editPseudo);
+
+    cy.get('cd-submit-button').click();
+
+    // Click the export and check its details table for updated content
+    this.getExpandCollapseElement(editPseudo).click();
+    cy.get('.active.tab-pane').should('contain.text', editPseudo);
+  }
+}
index 6395128c9473470e0f47accdc86a98e515ae2eda..176bca5a14774fd89c83bfbfc6b37fe0fb8681a3 100644 (file)
@@ -74,7 +74,7 @@ export abstract class PageHelper {
   }
 
   getTab(tabName: string) {
-    return cy.contains('.nav.nav-tabs li', new RegExp(`^${tabName}$`));
+    return cy.contains('.nav.nav-tabs li', tabName);
   }
 
   getTabText(index: number) {
@@ -203,6 +203,11 @@ export abstract class PageHelper {
     );
   }
 
+  existTableCell(name: string, oughtToBePresent = true) {
+    const waitRule = oughtToBePresent ? 'be.visible' : 'not.exist';
+    this.getFirstTableCell(name).should(waitRule);
+  }
+
   getExpandCollapseElement(content?: string) {
     this.waitDataTableToLoad();
 
index e5a28bfd4e20c24c3dc8dec3f15497db4fac18d3..b4c3c75ac5b853d5165870be8601701a51aac468 100644 (file)
@@ -30,19 +30,19 @@ describe('Pools page', () => {
 
   describe('Create, update and destroy', () => {
     it('should create a pool', () => {
-      pools.exist(poolName, false);
+      pools.existTableCell(poolName, false);
       pools.navigateTo('create');
       pools.create(poolName, 8, 'rbd');
-      pools.exist(poolName, true);
+      pools.existTableCell(poolName);
     });
 
     it('should edit a pools placement group', () => {
-      pools.exist(poolName, true);
+      pools.existTableCell(poolName);
       pools.edit_pool_pg(poolName, 32);
     });
 
     it('should show updated configuration field values', () => {
-      pools.exist(poolName, true);
+      pools.existTableCell(poolName);
       const bpsLimit = '4 B/s';
       pools.edit_pool_configuration(poolName, bpsLimit);
     });
index ccf858b41206d9bdcf233ad3a05fe567b6628209..98cee470eda993bdde355243cbf78e527aa757ae 100644 (file)
@@ -13,12 +13,6 @@ export class PoolPageHelper extends PageHelper {
     return expect((n & (n - 1)) === 0, `Placement groups ${n} are not a power of 2`).to.be.true;
   }
 
-  @PageHelper.restrictTo(pages.index.url)
-  exist(name: string, oughtToBePresent = true) {
-    const waitRule = oughtToBePresent ? 'be.visible' : 'not.exist';
-    this.getFirstTableCell(name).should(waitRule);
-  }
-
   @PageHelper.restrictTo(pages.create.url)
   create(name: string, placement_groups: number, ...apps: string[]) {
     cy.get('input[name=name]').clear().type(name);
index e8e340a7dc8dc108e0c6645d9156c371b8f12cb5..99c0903dacf49682cf3c9cd7c98c554aea22a677 100644 (file)
           </div>
         </div>
 
-        <!-- NFS -->
-        <ng-container *ngIf="!serviceForm.controls.unmanaged.value && serviceForm.controls.service_type.value === 'nfs'">
-          <!-- pool -->
-          <div class="form-group row">
-            <label i18n
-                   class="cd-col-form-label required"
-                   for="pool">Pool</label>
-            <div class="cd-col-form-input">
-              <select id="pool"
-                      name="pool"
-                      class="form-control custom-select"
-                      formControlName="pool">
-                <option *ngIf="pools === null"
-                        [ngValue]="null"
-                        i18n>Loading...</option>
-                <option *ngIf="pools !== null && pools.length === 0"
-                        [ngValue]="null"
-                        i18n>-- No pools available --</option>
-                <option *ngIf="pools !== null && pools.length > 0"
-                        [ngValue]="null"
-                        i18n>-- Select a pool --</option>
-                <option *ngFor="let pool of pools"
-                        [value]="pool.pool_name">{{ pool.pool_name }}</option>
-              </select>
-              <span class="invalid-feedback"
-                    *ngIf="serviceForm.showError('pool', frm, 'required')"
-                    i18n>This field is required.</span>
-            </div>
-          </div>
-
-          <!-- namespace -->
-          <div class="form-group row">
-            <label i18n
-                   class="cd-col-form-label"
-                   for="namespace">Namespace</label>
-            <div class="cd-col-form-input">
-              <input id="namespace"
-                     class="form-control"
-                     type="text"
-                     formControlName="namespace">
-            </div>
-          </div>
-        </ng-container>
-
         <!-- RGW -->
         <ng-container *ngIf="!serviceForm.controls.unmanaged.value && serviceForm.controls.service_type.value === 'rgw'">
           <!-- rgw_frontend_port -->
index 78863435ea328e06f92a2dd0d6ac0ae60b1a2204..fd3bc8025dbe04b661d3d2826ca3acc35716518c 100644 (file)
@@ -144,28 +144,14 @@ describe('ServiceFormComponent', () => {
     describe('should test service nfs', () => {
       beforeEach(() => {
         formHelper.setValue('service_type', 'nfs');
-        formHelper.setValue('pool', 'foo');
       });
 
-      it('should submit nfs with namespace', () => {
-        formHelper.setValue('namespace', 'bar');
+      it('should submit nfs', () => {
         component.onSubmit();
         expect(cephServiceService.create).toHaveBeenCalledWith({
           service_type: 'nfs',
           placement: {},
-          unmanaged: false,
-          pool: 'foo',
-          namespace: 'bar'
-        });
-      });
-
-      it('should submit nfs w/o namespace', () => {
-        component.onSubmit();
-        expect(cephServiceService.create).toHaveBeenCalledWith({
-          service_type: 'nfs',
-          placement: {},
-          unmanaged: false,
-          pool: 'foo'
+          unmanaged: false
         });
       });
     });
index 2b424d7f26a3546baebebeb1fa43b55f4b79f6c0..da4daf9c1f5f4ad8bf1d99dfbb4b005c3eb2a8ec 100644 (file)
@@ -115,22 +115,16 @@ export class ServiceFormComponent extends CdForm implements OnInit {
       hosts: [[]],
       count: [null, [CdValidators.number(false), Validators.min(1)]],
       unmanaged: [false],
-      // NFS & iSCSI
+      // iSCSI
       pool: [
         null,
         [
-          CdValidators.requiredIf({
-            service_type: 'nfs',
-            unmanaged: false
-          }),
           CdValidators.requiredIf({
             service_type: 'iscsi',
             unmanaged: false
           })
         ]
       ],
-      // NFS
-      namespace: [null],
       // RGW
       rgw_frontend_port: [
         null,
@@ -327,12 +321,6 @@ export class ServiceFormComponent extends CdForm implements OnInit {
         serviceSpec['placement']['count'] = values['count'];
       }
       switch (serviceType) {
-        case 'nfs':
-          serviceSpec['pool'] = values['pool'];
-          if (_.isString(values['namespace']) && !_.isEmpty(values['namespace'])) {
-            serviceSpec['namespace'] = values['namespace'];
-          }
-          break;
         case 'rgw':
           if (_.isNumber(values['rgw_frontend_port']) && values['rgw_frontend_port'] > 0) {
             serviceSpec['rgw_frontend_port'] = values['rgw_frontend_port'];
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/models/nfs.fsal.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/models/nfs.fsal.ts
new file mode 100644 (file)
index 0000000..f204ac6
--- /dev/null
@@ -0,0 +1,5 @@
+export interface NfsFSAbstractionLayer {
+  value: string;
+  descr: string;
+  disabled: boolean;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-cluster-type.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-cluster-type.enum.ts
deleted file mode 100644 (file)
index 7a775e5..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-export enum NFSClusterType {
-  user = 'user',
-  orchestrator = 'orchestrator'
-}
index 3abae2ee88e999e82faf3ad1e4ceac5b93b1d894..fcf5305393cba5a345a07bab57369e11bafb3d48 100644 (file)
@@ -25,18 +25,15 @@ describe('NfsDetailsComponent', () => {
     fixture = TestBed.createComponent(NfsDetailsComponent);
     component = fixture.componentInstance;
 
-    component.selection = undefined;
     component.selection = {
       export_id: 1,
       path: '/qwe',
       fsal: { name: 'CEPH', user_id: 'fs', fs_name: 1 },
       cluster_id: 'cluster1',
-      daemons: ['node1', 'node2'],
       pseudo: '/qwe',
-      tag: 'asd',
       access_type: 'RW',
       squash: 'no_root_squash',
-      protocols: [3, 4],
+      protocols: [4],
       transports: ['TCP', 'UDP'],
       clients: [
         {
@@ -44,9 +41,7 @@ describe('NfsDetailsComponent', () => {
           access_type: 'RW',
           squash: 'root_id_squash'
         }
-      ],
-      id: 'cluster1:1',
-      state: 'LOADING'
+      ]
     };
     component.ngOnChanges();
     fixture.detectChanges();
@@ -62,8 +57,7 @@ describe('NfsDetailsComponent', () => {
       'CephFS Filesystem': 1,
       'CephFS User': 'fs',
       Cluster: 'cluster1',
-      Daemons: ['node1', 'node2'],
-      'NFS Protocol': ['NFSv3', 'NFSv4'],
+      'NFS Protocol': ['NFSv4'],
       Path: '/qwe',
       Pseudo: '/qwe',
       'Security Label': undefined,
@@ -77,7 +71,7 @@ describe('NfsDetailsComponent', () => {
     const newData = _.assignIn(component.selection, {
       fsal: {
         name: 'RGW',
-        rgw_user_id: 'rgw_user_id'
+        user_id: 'user-id'
       }
     });
     component.selection = newData;
@@ -85,9 +79,8 @@ describe('NfsDetailsComponent', () => {
     expect(component.data).toEqual({
       'Access Type': 'RW',
       Cluster: 'cluster1',
-      Daemons: ['node1', 'node2'],
-      'NFS Protocol': ['NFSv3', 'NFSv4'],
-      'Object Gateway User': 'rgw_user_id',
+      'NFS Protocol': ['NFSv4'],
+      'Object Gateway User': 'user-id',
       Path: '/qwe',
       Pseudo: '/qwe',
       Squash: 'no_root_squash',
index 25a42416f7e36e68212f23fdf7a556ffead99371..5a84bd52e9da96f90da70cd8f48484edea83840f 100644 (file)
@@ -45,7 +45,6 @@ export class NfsDetailsComponent implements OnChanges {
 
       this.data = {};
       this.data[$localize`Cluster`] = this.selectedItem.cluster_id;
-      this.data[$localize`Daemons`] = this.selectedItem.daemons;
       this.data[$localize`NFS Protocol`] = this.selectedItem.protocols.map(
         (protocol: string) => 'NFSv' + protocol
       );
@@ -62,7 +61,7 @@ export class NfsDetailsComponent implements OnChanges {
         this.data[$localize`Security Label`] = this.selectedItem.fsal.sec_label_xattr;
       } else {
         this.data[$localize`Storage Backend`] = $localize`Object Gateway`;
-        this.data[$localize`Object Gateway User`] = this.selectedItem.fsal.rgw_user_id;
+        this.data[$localize`Object Gateway User`] = this.selectedItem.fsal.user_id;
       }
     }
   }
index 4f84f8e03b75ee86ae1905a4f18fdb9bb301354c..137cc43fa4b4f95bff5dd3168b38b62b46b0dfdf 100644 (file)
@@ -26,7 +26,7 @@
             <!-- Addresses -->
             <div class="form-group row">
               <label i18n
-                     class="cd-col-form-label"
+                     class="cd-col-form-label required"
                      for="addresses">Addresses</label>
               <div class="cd-col-form-input">
                 <input type="text"
 
             <!-- Squash -->
             <div class="form-group row">
-              <label i18n
-                     class="cd-col-form-label"
-                     for="squash">Squash</label>
+              <label class="cd-col-form-label"
+                     for="squash">
+                <span i18n>Squash</span>
+                <ng-container *ngTemplateOutlet="squashHelperTpl"></ng-container>
+              </label>
               <div class="cd-col-form-input">
                 <select class="form-control custom-select"
                         name="squash"
@@ -94,7 +96,8 @@
       <div class="col-12">
         <div class="float-right">
           <button class="btn btn-light "
-                  (click)="addClient()">
+                  (click)="addClient()"
+                  name="add_client">
             <i [ngClass]="[icons.add]"></i>
             <ng-container i18n>Add clients</ng-container>
           </button>
index 8423f177567952303c4f9979174d73d8492393ea..987acc957817fe95e066a647e808400b6339b310 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, Input, OnInit } from '@angular/core';
+import { Component, ContentChild, Input, OnInit, TemplateRef } from '@angular/core';
 import { FormArray, FormControl, NgForm, Validators } from '@angular/forms';
 
 import _ from 'lodash';
@@ -19,6 +19,8 @@ export class NfsFormClientComponent implements OnInit {
   @Input()
   clients: any[];
 
+  @ContentChild('squashHelper', { static: true }) squashHelperTpl: TemplateRef<any>;
+
   nfsSquash: any[] = this.nfsService.nfsSquash;
   nfsAccessType: any[] = this.nfsService.nfsAccessType;
   icons = Icons;
index d417d9ac32dded74e1f558c71e16f666273cf1de..3e390db7335e90ea480c302e177734b702248732 100644 (file)
 
       <div class="card-body">
         <!-- cluster_id -->
-        <div class="form-group row"
-             *ngIf="!isDefaultCluster">
-          <label class="cd-col-form-label required"
-                 for="cluster_id"
-                 i18n>Cluster</label>
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="cluster_id">
+            <span class="required"
+                  i18n>Cluster</span>
+            <cd-helper>
+              <p i18n>This is the ID of an NFS Service.</p>
+            </cd-helper>
+          </label>
           <div class="cd-col-form-input">
             <select class="form-control custom-select"
                     formControlName="cluster_id"
                     name="cluster_id"
-                    id="cluster_id"
-                    (change)="onClusterChange()">
+                    id="cluster_id">
               <option *ngIf="allClusters === null"
                       value=""
                       i18n>Loading...</option>
                       [value]="cluster.cluster_id">{{ cluster.cluster_id }}</option>
             </select>
             <span class="invalid-feedback"
-                  *ngIf="nfsForm.showError('cluster_id', formDir, 'required')"
-                  i18n>This field is required.</span>
-          </div>
-        </div>
-
-        <!-- daemons -->
-        <div class="form-group row"
-             *ngIf="clusterType">
-          <label class="cd-col-form-label"
-                 for="daemons">
-            <ng-container i18n>Daemons</ng-container>
-          </label>
-          <div class="cd-col-form-input">
-            <ng-container *ngFor="let daemon of nfsForm.getValue('daemons'); let i = index">
-              <div class="input-group cd-mb">
-                <input class="cd-form-control"
-                       type="text"
-                       [value]="daemon"
-                       disabled />
-                <span *ngIf="clusterType === 'user'"
-                      class="input-group-append">
-                  <button class="btn btn-light"
-                          type="button"
-                          (click)="removeDaemon(i, daemon)">
-                    <i [ngClass]="[icons.destroy]"
-                       aria-hidden="true"></i>
-                  </button>
-                </span>
-              </div>
-            </ng-container>
-
-            <div *ngIf="clusterType === 'user'"
-                 class="row">
-              <div class="col-md-12">
-                <cd-select [data]="nfsForm.get('daemons').value"
-                           [options]="daemonsSelections"
-                           [messages]="daemonsMessages"
-                           (selection)="onDaemonSelection()"
-                           elemClass="btn btn-light float-right">
-                  <i [ngClass]="[icons.add]"></i>
-                  <ng-container i18n>Add daemon</ng-container>
-                </cd-select>
-              </div>
-            </div>
-
-            <div *ngIf="clusterType === 'orchestrator'"
-                 class="row">
-              <div class="col-md-12">
-                <button type="button"
-                        class="btn btn-light float-right"
-                        (click)="onToggleAllDaemonsSelection()">
-                  <i [ngClass]="[icons.add]"></i>
-                  <ng-container *ngIf="nfsForm.getValue('daemons').length === 0; else hasDaemons"
-                                i18n>Add all daemons</ng-container>
-                  <ng-template #hasDaemons>
-                    <ng-container i18n>Remove all daemons</ng-container>
-                  </ng-template>
-                </button>
-              </div>
-            </div>
+                  *ngIf="nfsForm.showError('cluster_id', formDir, 'required') || allClusters?.length === 0"
+                  i18n>This field is required.
+                       To create a new NFS cluster, <a routerLink="/services/create"
+                                                       class="btn-link">add a new NFS Service</a>.</span>
           </div>
         </div>
 
                         value=""
                         i18n>-- Select the storage backend --</option>
                 <option *ngFor="let fsal of allFsals"
-                        [value]="fsal.value">{{ fsal.descr }}</option>
+                        [value]="fsal.value"
+                        [disabled]="fsal.disabled">{{ fsal.descr }}</option>
               </select>
               <span class="invalid-feedback"
                     *ngIf="nfsForm.showError('name', formDir, 'required')"
                     i18n>This field is required.</span>
-            </div>
-          </div>
-
-          <!-- RGW user -->
-          <div class="form-group row"
-               *ngIf="nfsForm.getValue('name') === 'RGW'">
-            <label class="cd-col-form-label required"
-                   for="rgw_user_id"
-                   i18n>Object Gateway User</label>
-            <div class="cd-col-form-input">
-              <select class="form-control custom-select"
-                      formControlName="rgw_user_id"
-                      name="rgw_user_id"
-                      id="rgw_user_id"
-                      (change)="rgwUserIdChangeHandler()">
-                <option *ngIf="allRgwUsers === null"
-                        value=""
-                        i18n>Loading...</option>
-                <option *ngIf="allRgwUsers !== null && allRgwUsers.length === 0"
-                        value=""
-                        i18n>-- No users available --</option>
-                <option *ngIf="allRgwUsers !== null && allRgwUsers.length > 0"
-                        value=""
-                        i18n>-- Select the object gateway user --</option>
-                <option *ngFor="let rgwUserId of allRgwUsers"
-                        [value]="rgwUserId">{{ rgwUserId }}</option>
-              </select>
               <span class="invalid-feedback"
-                    *ngIf="nfsForm.showError('rgw_user_id', formDir, 'required')"
-                    i18n>This field is required.</span>
-            </div>
-          </div>
-
-          <!-- CephFS user_id -->
-          <div class="form-group row"
-               *ngIf="nfsForm.getValue('name') === 'CEPH'">
-            <label class="cd-col-form-label required"
-                   for="user_id"
-                   i18n>CephFS User ID</label>
-            <div class="cd-col-form-input">
-              <select class="form-control custom-select"
-                      formControlName="user_id"
-                      name="user_id"
-                      id="user_id">
-                <option *ngIf="allCephxClients === null"
-                        value=""
-                        i18n>Loading...</option>
-                <option *ngIf="allCephxClients !== null && allCephxClients.length === 0"
-                        value=""
-                        i18n>-- No clients available --</option>
-                <option *ngIf="allCephxClients !== null && allCephxClients.length > 0"
-                        value=""
-                        i18n>-- Select the cephx client --</option>
-                <option *ngFor="let client of allCephxClients"
-                        [value]="client">{{ client }}</option>
-              </select>
-              <span class="invalid-feedback"
-                    *ngIf="nfsForm.showError('user_id', formDir, 'required')"
-                    i18n>This field is required.</span>
+                    *ngIf="fsalAvailabilityError"
+                    i18n>{{ fsalAvailabilityError }}</span>
             </div>
           </div>
 
                *ngIf="nfsForm.getValue('name') === 'CEPH'">
             <label class="cd-col-form-label required"
                    for="fs_name"
-                   i18n>CephFS Name</label>
+                   i18n>Volume</label>
             <div class="cd-col-form-input">
               <select class="form-control custom-select"
                       formControlName="fs_name"
                       name="fs_name"
                       id="fs_name"
-                      (change)="rgwUserIdChangeHandler()">
+                      (change)="pathChangeHandler()">
                 <option *ngIf="allFsNames === null"
                         value=""
                         i18n>Loading...</option>
         <!-- Path -->
         <div class="form-group row"
              *ngIf="nfsForm.getValue('name') === 'CEPH'">
-          <label class="cd-col-form-label required"
-                 for="path"
-                 i18n>CephFS Path</label>
+          <label class="cd-col-form-label"
+                 for="path">
+            <span class="required"
+                  i18n>CephFS Path</span>
+            <cd-helper>
+              <p i18n>A path in a CephFS file system.</p>
+            </cd-helper>
+          </label>
           <div class="cd-col-form-input">
             <input type="text"
                    class="form-control"
                    name="path"
                    id="path"
+                   data-testid="fs_path"
                    formControlName="path"
                    [ngbTypeahead]="pathDataSource"
                    (selectItem)="pathChangeHandler()"
                   *ngIf="nfsForm.showError('path', formDir, 'pattern')"
                   i18n>Path need to start with a '/' and can be followed by a word</span>
             <span class="form-text text-muted"
-                  *ngIf="isNewDirectory && !nfsForm.showError('path', formDir)"
-                  i18n>New directory will be created</span>
+                  *ngIf="nfsForm.showError('path', formDir, 'pathNameNotAllowed')"
+                  i18n>The path does not exist.</span>
           </div>
         </div>
 
         <!-- Bucket -->
         <div class="form-group row"
              *ngIf="nfsForm.getValue('name') === 'RGW'">
-          <label class="cd-col-form-label required"
-                 for="path"
-                 i18n>Path</label>
+          <label class="cd-col-form-label"
+                 for="path">
+            <span class="required"
+                  i18n>Bucket</span>
+          </label>
           <div class="cd-col-form-input">
             <input type="text"
                    class="form-control"
                    name="path"
                    id="path"
+                   data-testid="rgw_path"
                    formControlName="path"
-                   [ngbTypeahead]="bucketDataSource"
-                   (selectItem)="bucketChangeHandler()"
-                   (blur)="bucketChangeHandler()">
+                   [ngbTypeahead]="bucketDataSource">
 
             <span class="invalid-feedback"
                   *ngIf="nfsForm.showError('path', formDir, 'required')"
                   i18n>This field is required.</span>
-
             <span class="invalid-feedback"
-                  *ngIf="nfsForm.showError('path', formDir, 'pattern')"
-                  i18n>Path can only be a single '/' or a word</span>
-
-            <span class="form-text text-muted"
-                  *ngIf="isNewBucket && !nfsForm.showError('path', formDir)"
-                  i18n>New bucket will be created</span>
+                  *ngIf="nfsForm.showError('path', formDir, 'bucketNameNotAllowed')"
+                  i18n>The bucket does not exist or is not in the default realm (if multiple realms are configured).
+                       To continue, <a routerLink="/rgw/bucket/create"
+                                       class="btn-link">create a new bucket</a>.</span>
           </div>
         </div>
 
                  for="protocols"
                  i18n>NFS Protocol</label>
           <div class="cd-col-form-input">
-            <div class="custom-control custom-checkbox">
-              <input type="checkbox"
-                     class="custom-control-input"
-                     id="protocolNfsv3"
-                     name="protocolNfsv3"
-                     formControlName="protocolNfsv3"
-                     disabled>
-              <label i18n
-                     class="custom-control-label"
-                     for="protocolNfsv3">NFSv3</label>
-            </div>
             <div class="custom-control custom-checkbox">
               <input type="checkbox"
                      class="custom-control-input"
                      formControlName="protocolNfsv4"
                      name="protocolNfsv4"
-                     id="protocolNfsv4">
+                     id="protocolNfsv4"
+                     disabled>
               <label i18n
                      class="custom-control-label"
                      for="protocolNfsv4">NFSv4</label>
             </div>
             <span class="invalid-feedback"
-                  *ngIf="nfsForm.showError('protocolNfsv3', formDir, 'required') ||
-                  nfsForm.showError('protocolNfsv4', formDir, 'required')"
+                  *ngIf="nfsForm.showError('protocolNfsv4', formDir, 'required')"
                   i18n>This field is required.</span>
           </div>
         </div>
 
-        <!-- Tag -->
-        <div class="form-group row"
-             *ngIf="nfsForm.getValue('protocolNfsv3')">
-          <label class="cd-col-form-label"
-                 for="tag">
-            <ng-container i18n>NFS Tag</ng-container>
-            <cd-helper>
-              <p i18n>Alternative access for <strong>NFS v3</strong> mounts (it must not have a leading /).</p>
-              <p i18n>Clients may not mount subdirectories (i.e. if Tag = foo, the client may not mount foo/baz).</p>
-              <p i18n>By using different Tag options, the same Path may be exported multiple times.</p>
-            </cd-helper>
-          </label>
-          <div class="cd-col-form-input">
-            <input type="text"
-                   class="form-control"
-                   name="tag"
-                   id="tag"
-                   formControlName="tag">
-          </div>
-        </div>
-
         <!-- Pseudo -->
         <div class="form-group row"
              *ngIf="nfsForm.getValue('protocolNfsv4')">
 
         <!-- Squash -->
         <div class="form-group row">
-          <label class="cd-col-form-label required"
-                 for="squash"
-                 i18n>Squash</label>
+          <label class="cd-col-form-label"
+                 for="squash">
+            <span class="required"
+                  i18n>Squash</span>
+            <ng-container *ngTemplateOutlet="squashHelper"></ng-container>
+          </label>
           <div class="cd-col-form-input">
             <select class="form-control custom-select"
                     name="squash"
         <cd-nfs-form-client [form]="nfsForm"
                             [clients]="clients"
                             #nfsClients>
+          <ng-template #squashHelper>
+            <cd-helper>
+              <ul class="squash-helper">
+                <li>
+                  <span class="squash-helper-item-value">no_root_squash: </span>
+                  <span i18n>No user id squashing is performed.</span>
+                </li>
+                <li>
+                  <span class="squash-helper-item-value">root_id_squash: </span>
+                  <span i18n>uid 0 and gid 0 are squashed to the Anonymous_Uid and Anonymous_Gid gid 0 in alt_groups lists is also squashed.</span>
+                </li>
+                <li>
+                  <span class="squash-helper-item-value">root_squash: </span>
+                  <span i18n>uid 0 and gid of any value are squashed to the Anonymous_Uid and Anonymous_Gid alt_groups lists is discarded.</span>
+                </li>
+                <li>
+                  <span class="squash-helper-item-value">all_squash: </span>
+                  <span i18n>All users are squashed.</span>
+                </li>
+              </ul>
+            </cd-helper>
+          </ng-template>
         </cd-nfs-form-client>
 
       </div>
index cebcc8877a217ba752a01478da5bfe4d296ed08c..4d892a120fc64f735d4ba921574256f677c9f8c6 100644 (file)
@@ -1,3 +1,11 @@
 .cd-mb {
   margin-bottom: 10px;
 }
+
+.squash-helper {
+  padding-left: 1rem;
+}
+
+.squash-helper-item-value {
+  font-weight: bold;
+}
index e567abfb562b69cea1be27e0608d46da8ef91f56..4bf34e2c7a0d1fafa1a769fcb4f837a9854b9adf 100644 (file)
@@ -6,14 +6,15 @@ import { RouterTestingModule } from '@angular/router/testing';
 
 import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
 import { ToastrModule } from 'ngx-toastr';
+import { Observable, of } from 'rxjs';
 
+import { NfsFormClientComponent } from '~/app/ceph/nfs/nfs-form-client/nfs-form-client.component';
+import { NfsFormComponent } from '~/app/ceph/nfs/nfs-form/nfs-form.component';
+import { Directory } from '~/app/shared/api/nfs.service';
 import { LoadingPanelComponent } from '~/app/shared/components/loading-panel/loading-panel.component';
 import { SharedModule } from '~/app/shared/shared.module';
 import { ActivatedRouteStub } from '~/testing/activated-route-stub';
 import { configureTestBed, RgwHelper } from '~/testing/unit-test-helper';
-import { NFSClusterType } from '../nfs-cluster-type.enum';
-import { NfsFormClientComponent } from '../nfs-form-client/nfs-form-client.component';
-import { NfsFormComponent } from './nfs-form.component';
 
 describe('NfsFormComponent', () => {
   let component: NfsFormComponent;
@@ -50,38 +51,9 @@ describe('NfsFormComponent', () => {
     RgwHelper.selectDaemon();
     fixture.detectChanges();
 
-    httpTesting.expectOne('api/nfs-ganesha/daemon').flush([
-      { daemon_id: 'node1', cluster_id: 'cluster1', cluster_type: NFSClusterType.user },
-      { daemon_id: 'node2', cluster_id: 'cluster1', cluster_type: NFSClusterType.user },
-      { daemon_id: 'node5', cluster_id: 'cluster2', cluster_type: NFSClusterType.orchestrator }
-    ]);
     httpTesting.expectOne('ui-api/nfs-ganesha/fsals').flush(['CEPH', 'RGW']);
-    httpTesting.expectOne('ui-api/nfs-ganesha/cephx/clients').flush(['admin', 'fs', 'rgw']);
     httpTesting.expectOne('ui-api/nfs-ganesha/cephfs/filesystems').flush([{ id: 1, name: 'a' }]);
-    httpTesting
-      .expectOne(`api/rgw/user?${RgwHelper.DAEMON_QUERY_PARAM}`)
-      .flush(['test', 'dev', 'tenant$user']);
-    const user_dev = {
-      suspended: 0,
-      user_id: 'dev',
-      keys: ['a']
-    };
-    httpTesting.expectOne(`api/rgw/user/dev?${RgwHelper.DAEMON_QUERY_PARAM}`).flush(user_dev);
-    const user_test = {
-      suspended: 1,
-      user_id: 'test',
-      keys: ['a']
-    };
-    httpTesting.expectOne(`api/rgw/user/test?${RgwHelper.DAEMON_QUERY_PARAM}`).flush(user_test);
-    const tenantUser = {
-      suspended: 0,
-      tenant: 'tenant',
-      user_id: 'user',
-      keys: ['a']
-    };
-    httpTesting
-      .expectOne(`api/rgw/user/tenant%24user?${RgwHelper.DAEMON_QUERY_PARAM}`)
-      .flush(tenantUser);
+    httpTesting.expectOne('api/nfs-ganesha/cluster').flush(['mynfs']);
     httpTesting.verify();
   });
 
@@ -90,15 +62,12 @@ describe('NfsFormComponent', () => {
   });
 
   it('should process all data', () => {
-    expect(component.allDaemons).toEqual({ cluster1: ['node1', 'node2'], cluster2: ['node5'] });
-    expect(component.isDefaultCluster).toEqual(false);
     expect(component.allFsals).toEqual([
-      { descr: 'CephFS', value: 'CEPH' },
-      { descr: 'Object Gateway', value: 'RGW' }
+      { descr: 'CephFS', value: 'CEPH', disabled: false },
+      { descr: 'Object Gateway', value: 'RGW', disabled: false }
     ]);
-    expect(component.allCephxClients).toEqual(['admin', 'fs', 'rgw']);
     expect(component.allFsNames).toEqual([{ id: 1, name: 'a' }]);
-    expect(component.allRgwUsers).toEqual(['dev', 'tenant$user']);
+    expect(component.allClusters).toEqual([{ cluster_id: 'mynfs' }]);
   });
 
   it('should create the form', () => {
@@ -106,16 +75,13 @@ describe('NfsFormComponent', () => {
       access_type: 'RW',
       clients: [],
       cluster_id: '',
-      daemons: [],
-      fsal: { fs_name: 'a', name: '', rgw_user_id: '', user_id: '' },
-      path: '',
-      protocolNfsv3: false,
+      fsal: { fs_name: 'a', name: '' },
+      path: '/',
       protocolNfsv4: true,
       pseudo: '',
       sec_label_xattr: 'security.selinux',
       security_label: false,
       squash: '',
-      tag: '',
       transportTCP: true,
       transportUDP: true
     });
@@ -123,31 +89,9 @@ describe('NfsFormComponent', () => {
   });
 
   it('should prepare data when selecting an cluster', () => {
-    expect(component.allDaemons).toEqual({ cluster1: ['node1', 'node2'], cluster2: ['node5'] });
-    expect(component.daemonsSelections).toEqual([]);
-    expect(component.clusterType).toBeNull();
-
     component.nfsForm.patchValue({ cluster_id: 'cluster1' });
-    component.onClusterChange();
-
-    expect(component.daemonsSelections).toEqual([
-      { description: '', name: 'node1', selected: false, enabled: true },
-      { description: '', name: 'node2', selected: false, enabled: true }
-    ]);
-    expect(component.clusterType).toBe(NFSClusterType.user);
 
     component.nfsForm.patchValue({ cluster_id: 'cluster2' });
-    component.onClusterChange();
-    expect(component.clusterType).toBe(NFSClusterType.orchestrator);
-    expect(component.daemonsSelections).toEqual([]);
-  });
-
-  it('should clean data when changing cluster', () => {
-    component.nfsForm.patchValue({ cluster_id: 'cluster1', daemons: ['node1'] });
-    component.nfsForm.patchValue({ cluster_id: 'node2' });
-    component.onClusterChange();
-
-    expect(component.nfsForm.getValue('daemons')).toEqual([]);
   });
 
   it('should not allow changing cluster in edit mode', () => {
@@ -156,13 +100,8 @@ describe('NfsFormComponent', () => {
     expect(component.nfsForm.get('cluster_id').disabled).toBeTruthy();
   });
 
-  it('should mark NFSv4 protocol as required', () => {
-    component.nfsForm.patchValue({
-      protocolNfsv4: false
-    });
-    component.nfsForm.updateValueAndValidity({ emitEvent: false });
-    expect(component.nfsForm.valid).toBeFalsy();
-    expect(component.nfsForm.get('protocolNfsv4').hasError('required')).toBeTruthy();
+  it('should mark NFSv4 protocol as enabled always', () => {
+    expect(component.nfsForm.get('protocolNfsv4')).toBeTruthy();
   });
 
   describe('should submit request', () => {
@@ -171,29 +110,16 @@ describe('NfsFormComponent', () => {
         access_type: 'RW',
         clients: [],
         cluster_id: 'cluster1',
-        daemons: ['node2'],
-        fsal: { name: 'CEPH', user_id: 'fs', fs_name: 1, rgw_user_id: '' },
+        fsal: { name: 'CEPH', fs_name: 1 },
         path: '/foo',
-        protocolNfsv3: true,
         protocolNfsv4: true,
         pseudo: '/baz',
         squash: 'no_root_squash',
-        tag: 'bar',
         transportTCP: true,
         transportUDP: true
       });
     });
 
-    it('should remove "pseudo" requirement when NFS v4 disabled', () => {
-      component.nfsForm.patchValue({
-        protocolNfsv4: false,
-        pseudo: ''
-      });
-
-      component.nfsForm.updateValueAndValidity({ emitEvent: false });
-      expect(component.nfsForm.valid).toBeTruthy();
-    });
-
     it('should call update', () => {
       activatedRoute.setParams({ cluster_id: 'cluster1', export_id: '1' });
       component.isEdit = true;
@@ -208,15 +134,13 @@ describe('NfsFormComponent', () => {
         access_type: 'RW',
         clients: [],
         cluster_id: 'cluster1',
-        daemons: ['node2'],
-        export_id: '1',
-        fsal: { fs_name: 1, name: 'CEPH', sec_label_xattr: null, user_id: 'fs' },
+        export_id: 1,
+        fsal: { fs_name: 1, name: 'CEPH', sec_label_xattr: null },
         path: '/foo',
-        protocols: [3, 4],
+        protocols: [4],
         pseudo: '/baz',
         security_label: false,
         squash: 'no_root_squash',
-        tag: 'bar',
         transports: ['TCP', 'UDP']
       });
     });
@@ -231,21 +155,55 @@ describe('NfsFormComponent', () => {
         access_type: 'RW',
         clients: [],
         cluster_id: 'cluster1',
-        daemons: ['node2'],
         fsal: {
           fs_name: 1,
           name: 'CEPH',
-          sec_label_xattr: null,
-          user_id: 'fs'
+          sec_label_xattr: null
         },
         path: '/foo',
-        protocols: [3, 4],
+        protocols: [4],
         pseudo: '/baz',
         security_label: false,
         squash: 'no_root_squash',
-        tag: 'bar',
         transports: ['TCP', 'UDP']
       });
     });
   });
+
+  describe('pathExistence', () => {
+    beforeEach(() => {
+      component['nfsService']['lsDir'] = jest.fn(
+        (): Observable<Directory> => of({ paths: ['/path1'] })
+      );
+      component.nfsForm.get('name').setValue('CEPH');
+      component.setPathValidation();
+    });
+
+    const testValidator = (pathName: string, valid: boolean, expectedError?: string) => {
+      const path = component.nfsForm.get('path');
+      path.setValue(pathName);
+      path.markAsDirty();
+      path.updateValueAndValidity();
+
+      if (valid) {
+        expect(path.errors).toBe(null);
+      } else {
+        expect(path.hasError(expectedError)).toBeTruthy();
+      }
+    };
+
+    it('path cannot be empty', () => {
+      testValidator('', false, 'required');
+    });
+
+    it('path that does not exist should be invalid', () => {
+      testValidator('/path2', false, 'pathNameNotAllowed');
+      expect(component['nfsService']['lsDir']).toHaveBeenCalledTimes(1);
+    });
+
+    it('path that exists should be valid', () => {
+      testValidator('/path1', true);
+      expect(component['nfsService']['lsDir']).toHaveBeenCalledTimes(1);
+    });
+  });
 });
index 3e23b9878b95d82c9bdc3feb6efe9e679d97aae2..46eeeec52cb22f839ca3afe6dc0e39864bbde455 100644 (file)
@@ -1,15 +1,21 @@
 import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
-import { FormControl, Validators } from '@angular/forms';
+import {
+  AbstractControl,
+  AsyncValidatorFn,
+  FormControl,
+  ValidationErrors,
+  Validators
+} from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 
 import _ from 'lodash';
 import { forkJoin, Observable, of } from 'rxjs';
-import { debounceTime, distinctUntilChanged, map, mergeMap } from 'rxjs/operators';
+import { catchError, debounceTime, distinctUntilChanged, map, mergeMap } from 'rxjs/operators';
 
-import { NfsService } from '~/app/shared/api/nfs.service';
-import { RgwUserService } from '~/app/shared/api/rgw-user.service';
-import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
-import { SelectOption } from '~/app/shared/components/select/select-option.model';
+import { NfsFSAbstractionLayer } from '~/app/ceph/nfs/models/nfs.fsal';
+import { Directory, NfsService } from '~/app/shared/api/nfs.service';
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { RgwSiteService } from '~/app/shared/api/rgw-site.service';
 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
 import { Icons } from '~/app/shared/enum/icons.enum';
 import { CdForm } from '~/app/shared/forms/cd-form';
@@ -20,7 +26,6 @@ import { FinishedTask } from '~/app/shared/models/finished-task';
 import { Permission } from '~/app/shared/models/permissions';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
-import { NFSClusterType } from '../nfs-cluster-type.enum';
 import { NfsFormClientComponent } from '../nfs-form-client/nfs-form-client.component';
 
 @Component({
@@ -39,21 +44,14 @@ export class NfsFormComponent extends CdForm implements OnInit {
   isEdit = false;
 
   cluster_id: string = null;
-  clusterType: string = null;
   export_id: string = null;
 
-  isNewDirectory = false;
-  isNewBucket = false;
-  isDefaultCluster = false;
-
-  allClusters: { cluster_id: string; cluster_type: string }[] = null;
-  allDaemons = {};
+  allClusters: { cluster_id: string }[] = null;
   icons = Icons;
 
   allFsals: any[] = [];
-  allRgwUsers: any[] = [];
-  allCephxClients: any[] = null;
   allFsNames: any[] = null;
+  fsalAvailabilityError: string = null;
 
   defaultAccessType = { RGW: 'RO' };
   nfsAccessType: any[] = this.nfsService.nfsAccessType;
@@ -62,15 +60,12 @@ export class NfsFormComponent extends CdForm implements OnInit {
   action: string;
   resource: string;
 
-  daemonsSelections: SelectOption[] = [];
-  daemonsMessages = new SelectMessages({ noOptions: $localize`There are no daemons available.` });
-
   pathDataSource = (text$: Observable<string>) => {
     return text$.pipe(
       debounceTime(200),
       distinctUntilChanged(),
       mergeMap((token: string) => this.getPathTypeahead(token)),
-      map((val: any) => val.paths)
+      map((val: string[]) => val)
     );
   };
 
@@ -87,7 +82,8 @@ export class NfsFormComponent extends CdForm implements OnInit {
     private nfsService: NfsService,
     private route: ActivatedRoute,
     private router: Router,
-    private rgwUserService: RgwUserService,
+    private rgwBucketService: RgwBucketService,
+    private rgwSiteService: RgwSiteService,
     private formBuilder: CdFormBuilder,
     private taskWrapper: TaskWrapperService,
     private cdRef: ChangeDetectorRef,
@@ -101,9 +97,8 @@ export class NfsFormComponent extends CdForm implements OnInit {
 
   ngOnInit() {
     const promises: Observable<any>[] = [
-      this.nfsService.daemon(),
+      this.nfsService.listClusters(),
       this.nfsService.fsals(),
-      this.nfsService.clients(),
       this.nfsService.filesystems()
     ];
 
@@ -129,12 +124,11 @@ export class NfsFormComponent extends CdForm implements OnInit {
 
   getData(promises: Observable<any>[]) {
     forkJoin(promises).subscribe((data: any[]) => {
-      this.resolveDaemons(data[0]);
+      this.resolveClusters(data[0]);
       this.resolveFsals(data[1]);
-      this.resolveClients(data[2]);
-      this.resolveFilesystems(data[3]);
-      if (data[4]) {
-        this.resolveModel(data[4]);
+      this.resolveFilesystems(data[2]);
+      if (data[3]) {
+        this.resolveModel(data[3]);
       }
 
       this.loadingReady();
@@ -146,49 +140,20 @@ export class NfsFormComponent extends CdForm implements OnInit {
       cluster_id: new FormControl('', {
         validators: [Validators.required]
       }),
-      daemons: new FormControl([]),
       fsal: new CdFormGroup({
         name: new FormControl('', {
           validators: [Validators.required]
         }),
-        user_id: new FormControl('', {
-          validators: [
-            CdValidators.requiredIf({
-              name: 'CEPH'
-            })
-          ]
-        }),
         fs_name: new FormControl('', {
           validators: [
             CdValidators.requiredIf({
               name: 'CEPH'
             })
           ]
-        }),
-        rgw_user_id: new FormControl('', {
-          validators: [
-            CdValidators.requiredIf({
-              name: 'RGW'
-            })
-          ]
         })
       }),
-      path: new FormControl(''),
-      protocolNfsv3: new FormControl(false, {
-        validators: [
-          CdValidators.requiredIf({ protocolNfsv4: false }, (value: boolean) => {
-            return !value;
-          })
-        ]
-      }),
-      protocolNfsv4: new FormControl(true, {
-        validators: [
-          CdValidators.requiredIf({ protocolNfsv3: false }, (value: boolean) => {
-            return !value;
-          })
-        ]
-      }),
-      tag: new FormControl(''),
+      path: new FormControl('/'),
+      protocolNfsv4: new FormControl(true),
       pseudo: new FormControl('', {
         validators: [
           CdValidators.requiredIf({ protocolNfsv4: true }),
@@ -229,15 +194,6 @@ export class NfsFormComponent extends CdForm implements OnInit {
       res.sec_label_xattr = res.fsal.sec_label_xattr;
     }
 
-    if (this.clusterType === NFSClusterType.user) {
-      this.daemonsSelections = _.map(
-        this.allDaemons[res.cluster_id],
-        (daemon) => new SelectOption(res.daemons.indexOf(daemon) !== -1, daemon, '')
-      );
-      this.daemonsSelections = [...this.daemonsSelections];
-    }
-
-    res.protocolNfsv3 = res.protocols.indexOf(3) !== -1;
     res.protocolNfsv4 = res.protocols.indexOf(4) !== -1;
     delete res.protocols;
 
@@ -261,31 +217,10 @@ export class NfsFormComponent extends CdForm implements OnInit {
     this.clients = res.clients;
   }
 
-  resolveDaemons(daemons: Record<string, any>) {
-    daemons = _.sortBy(daemons, ['daemon_id']);
-    const clusters = _.groupBy(daemons, 'cluster_id');
-
+  resolveClusters(clusters: string[]) {
     this.allClusters = [];
-    _.forIn(clusters, (cluster, cluster_id) => {
-      this.allClusters.push({ cluster_id: cluster_id, cluster_type: cluster[0].cluster_type });
-      this.allDaemons[cluster_id] = [];
-    });
-
-    _.forEach(daemons, (daemon) => {
-      this.allDaemons[daemon.cluster_id].push(daemon.daemon_id);
-    });
-
-    if (this.isEdit) {
-      this.clusterType = _.find(this.allClusters, { cluster_id: this.cluster_id })?.cluster_type;
-    }
-
-    const hasOneCluster = _.isArray(this.allClusters) && this.allClusters.length === 1;
-    this.isDefaultCluster = hasOneCluster && this.allClusters[0].cluster_id === '_default_';
-    if (hasOneCluster) {
-      this.nfsForm.patchValue({
-        cluster_id: this.allClusters[0].cluster_id
-      });
-      this.onClusterChange();
+    for (const cluster of clusters) {
+      this.allClusters.push({ cluster_id: cluster });
     }
   }
 
@@ -297,16 +232,6 @@ export class NfsFormComponent extends CdForm implements OnInit {
 
       if (_.isObjectLike(fsalItem)) {
         this.allFsals.push(fsalItem);
-        if (fsalItem.value === 'RGW') {
-          this.rgwUserService.list().subscribe((result: any) => {
-            result.forEach((user: Record<string, any>) => {
-              if (user.suspended === 0 && user.keys.length > 0) {
-                const userId = user.tenant ? `${user.tenant}$${user.user_id}` : user.user_id;
-                this.allRgwUsers.push(userId);
-              }
-            });
-          });
-        }
       }
     });
 
@@ -317,10 +242,6 @@ export class NfsFormComponent extends CdForm implements OnInit {
     }
   }
 
-  resolveClients(clients: any[]) {
-    this.allCephxClients = clients;
-  }
-
   resolveFilesystems(filesystems: any[]) {
     this.allFsNames = filesystems;
     if (filesystems.length === 1) {
@@ -333,15 +254,56 @@ export class NfsFormComponent extends CdForm implements OnInit {
   }
 
   fsalChangeHandler() {
-    this.nfsForm.patchValue({
-      tag: this._generateTag(),
-      pseudo: this._generatePseudo(),
-      access_type: this._updateAccessType()
+    const fsalValue = this.nfsForm.getValue('name');
+    const checkAvailability =
+      fsalValue === 'RGW'
+        ? this.rgwSiteService.get('realms').pipe(
+            mergeMap((realms: string[]) =>
+              realms.length === 0
+                ? of(true)
+                : this.rgwSiteService.isDefaultRealm().pipe(
+                    mergeMap((isDefaultRealm) => {
+                      if (!isDefaultRealm) {
+                        throw new Error('Selected realm is not the default.');
+                      }
+                      return of(true);
+                    })
+                  )
+            )
+          )
+        : this.nfsService.filesystems();
+
+    checkAvailability.subscribe({
+      next: () => {
+        this.setFsalAvailability(fsalValue, true);
+        this.nfsForm.patchValue({
+          path: fsalValue === 'RGW' ? '' : '/',
+          pseudo: this.generatePseudo(),
+          access_type: this.updateAccessType()
+        });
+
+        this.setPathValidation();
+
+        this.cdRef.detectChanges();
+      },
+      error: (error) => {
+        this.setFsalAvailability(fsalValue, false, error);
+        this.nfsForm.get('name').setValue('');
+      }
     });
+  }
 
-    this.setPathValidation();
+  private setFsalAvailability(fsalValue: string, available: boolean, errorMessage: string = '') {
+    this.allFsals = this.allFsals.map((fsalItem: NfsFSAbstractionLayer) => {
+      if (fsalItem.value === fsalValue) {
+        fsalItem.disabled = !available;
 
-    this.cdRef.detectChanges();
+        this.fsalAvailabilityError = fsalItem.disabled
+          ? $localize`${fsalItem.descr} backend is not available. ${errorMessage}`
+          : null;
+      }
+      return fsalItem;
+    });
   }
 
   accessTypeChangeHandler() {
@@ -351,21 +313,17 @@ export class NfsFormComponent extends CdForm implements OnInit {
   }
 
   setPathValidation() {
+    const path = this.nfsForm.get('path');
+    path.setValidators([Validators.required]);
     if (this.nfsForm.getValue('name') === 'RGW') {
-      this.nfsForm
-        .get('path')
-        .setValidators([Validators.required, Validators.pattern('^(/|[^/><|&()#?]+)$')]);
+      path.setAsyncValidators([CdValidators.bucketExistence(true, this.rgwBucketService)]);
     } else {
-      this.nfsForm
-        .get('path')
-        .setValidators([Validators.required, Validators.pattern('^/[^><|&()?]*$')]);
+      path.setAsyncValidators([this.pathExistence(true)]);
     }
-  }
 
-  rgwUserIdChangeHandler() {
-    this.nfsForm.patchValue({
-      pseudo: this._generatePseudo()
-    });
+    if (this.isEdit) {
+      path.markAsDirty();
+    }
   }
 
   getAccessTypeHelp(accessType: string) {
@@ -387,60 +345,42 @@ export class NfsFormComponent extends CdForm implements OnInit {
     return '';
   }
 
-  getPathTypeahead(path: any) {
+  private getPathTypeahead(path: any) {
     if (!_.isString(path) || path === '/') {
       return of([]);
     }
 
     const fsName = this.nfsForm.getValue('fsal').fs_name;
-    return this.nfsService.lsDir(fsName, path);
+    return this.nfsService.lsDir(fsName, path).pipe(
+      map((result: Directory) =>
+        result.paths.filter((dirName: string) => dirName.toLowerCase().includes(path)).slice(0, 15)
+      ),
+      catchError(() => of([$localize`Error while retrieving paths.`]))
+    );
   }
 
   pathChangeHandler() {
     this.nfsForm.patchValue({
-      pseudo: this._generatePseudo()
-    });
-
-    const path = this.nfsForm.getValue('path');
-    this.getPathTypeahead(path).subscribe((res: any) => {
-      this.isNewDirectory = path !== '/' && res.paths.indexOf(path) === -1;
-    });
-  }
-
-  bucketChangeHandler() {
-    this.nfsForm.patchValue({
-      tag: this._generateTag(),
-      pseudo: this._generatePseudo()
-    });
-
-    const bucket = this.nfsForm.getValue('path');
-    this.getBucketTypeahead(bucket).subscribe((res: any) => {
-      this.isNewBucket = bucket !== '' && res.indexOf(bucket) === -1;
+      pseudo: this.generatePseudo()
     });
   }
 
-  getBucketTypeahead(path: string): Observable<any> {
-    const rgwUserId = this.nfsForm.getValue('rgw_user_id');
-
-    if (_.isString(rgwUserId) && _.isString(path) && path !== '/' && path !== '') {
-      return this.nfsService.buckets(rgwUserId);
+  private getBucketTypeahead(path: string): Observable<any> {
+    if (_.isString(path) && path !== '/' && path !== '') {
+      return this.rgwBucketService.list().pipe(
+        map((bucketList) =>
+          bucketList
+            .filter((bucketName: string) => bucketName.toLowerCase().includes(path))
+            .slice(0, 15)
+        ),
+        catchError(() => of([$localize`Error while retrieving bucket names.`]))
+      );
     } else {
       return of([]);
     }
   }
 
-  _generateTag() {
-    let newTag = this.nfsForm.getValue('tag');
-    if (!this.nfsForm.get('tag').dirty) {
-      newTag = undefined;
-      if (this.nfsForm.getValue('fsal') === 'RGW') {
-        newTag = this.nfsForm.getValue('path');
-      }
-    }
-    return newTag;
-  }
-
-  _generatePseudo() {
+  private generatePseudo() {
     let newPseudo = this.nfsForm.getValue('pseudo');
     if (this.nfsForm.get('pseudo') && !this.nfsForm.get('pseudo').dirty) {
       newPseudo = undefined;
@@ -449,19 +389,12 @@ export class NfsFormComponent extends CdForm implements OnInit {
         if (_.isString(this.nfsForm.getValue('path'))) {
           newPseudo += this.nfsForm.getValue('path');
         }
-      } else if (this.nfsForm.getValue('fsal') === 'RGW') {
-        if (_.isString(this.nfsForm.getValue('rgw_user_id'))) {
-          newPseudo = '/' + this.nfsForm.getValue('rgw_user_id');
-          if (_.isString(this.nfsForm.getValue('path'))) {
-            newPseudo += '/' + this.nfsForm.getValue('path');
-          }
-        }
       }
     }
     return newPseudo;
   }
 
-  _updateAccessType() {
+  private updateAccessType() {
     const name = this.nfsForm.getValue('name');
     let accessType = this.defaultAccessType[name];
 
@@ -472,57 +405,17 @@ export class NfsFormComponent extends CdForm implements OnInit {
     return accessType;
   }
 
-  onClusterChange() {
-    const cluster_id = this.nfsForm.getValue('cluster_id');
-    this.clusterType = _.find(this.allClusters, { cluster_id: cluster_id })?.cluster_type;
-    if (this.clusterType === NFSClusterType.user) {
-      this.daemonsSelections = _.map(
-        this.allDaemons[cluster_id],
-        (daemon) => new SelectOption(false, daemon, '')
-      );
-      this.daemonsSelections = [...this.daemonsSelections];
-    } else {
-      this.daemonsSelections = [];
-    }
-    this.nfsForm.patchValue({ daemons: [] });
-  }
-
-  removeDaemon(index: number, daemon: string) {
-    this.daemonsSelections.forEach((value) => {
-      if (value.name === daemon) {
-        value.selected = false;
-      }
-    });
-
-    const daemons = this.nfsForm.get('daemons');
-    daemons.value.splice(index, 1);
-    daemons.setValue(daemons.value);
-
-    return false;
-  }
-
-  onDaemonSelection() {
-    this.nfsForm.get('daemons').setValue(this.nfsForm.getValue('daemons'));
-  }
-
-  onToggleAllDaemonsSelection() {
-    const cluster_id = this.nfsForm.getValue('cluster_id');
-    const daemons =
-      this.nfsForm.getValue('daemons').length === 0 ? this.allDaemons[cluster_id] : [];
-    this.nfsForm.patchValue({ daemons: daemons });
-  }
-
   submitAction() {
     let action: Observable<any>;
-    const requestModel = this._buildRequest();
+    const requestModel = this.buildRequest();
 
     if (this.isEdit) {
       action = this.taskWrapper.wrapTaskAroundCall({
         task: new FinishedTask('nfs/edit', {
           cluster_id: this.cluster_id,
-          export_id: this.export_id
+          export_id: _.parseInt(this.export_id)
         }),
-        call: this.nfsService.update(this.cluster_id, this.export_id, requestModel)
+        call: this.nfsService.update(this.cluster_id, _.parseInt(this.export_id), requestModel)
       });
     } else {
       // Create
@@ -542,31 +435,18 @@ export class NfsFormComponent extends CdForm implements OnInit {
     });
   }
 
-  _buildRequest() {
+  private buildRequest() {
     const requestModel: any = _.cloneDeep(this.nfsForm.value);
 
-    if (_.isUndefined(requestModel.tag) || requestModel.tag === '') {
-      requestModel.tag = null;
-    }
-
     if (this.isEdit) {
-      requestModel.export_id = this.export_id;
+      requestModel.export_id = _.parseInt(this.export_id);
     }
 
-    if (requestModel.fsal.name === 'CEPH') {
-      delete requestModel.fsal.rgw_user_id;
-    } else {
+    if (requestModel.fsal.name === 'RGW') {
       delete requestModel.fsal.fs_name;
-      delete requestModel.fsal.user_id;
     }
 
     requestModel.protocols = [];
-    if (requestModel.protocolNfsv3) {
-      requestModel.protocols.push(3);
-    } else {
-      requestModel.tag = null;
-    }
-    delete requestModel.protocolNfsv3;
     if (requestModel.protocolNfsv4) {
       requestModel.protocols.push(4);
     } else {
@@ -613,4 +493,22 @@ export class NfsFormComponent extends CdForm implements OnInit {
 
     return requestModel;
   }
+
+  private pathExistence(requiredExistenceResult: boolean): AsyncValidatorFn {
+    return (control: AbstractControl): Observable<ValidationErrors | null> => {
+      if (control.pristine || !control.value) {
+        return of({ required: true });
+      }
+      const fsName = this.nfsForm.getValue('fsal').fs_name;
+      return this.nfsService
+        .lsDir(fsName, control.value)
+        .pipe(
+          map((directory: Directory) =>
+            directory.paths.includes(control.value) === requiredExistenceResult
+              ? null
+              : { pathNameNotAllowed: true }
+          )
+        );
+    };
+  }
 }
index d02a05c23d82a8d7fe5f86ede12d609bcacd7ed2..5e43cdd658cb45d71a598751fc454bceecb61fea 100644 (file)
@@ -58,7 +58,6 @@ describe('NfsListComponent', () => {
     beforeEach(() => {
       fixture.detectChanges();
       spyOn(nfsService, 'list').and.callThrough();
-      httpTesting.expectOne('api/nfs-ganesha/daemon').flush([]);
     });
 
     afterEach(() => {
@@ -126,9 +125,6 @@ describe('NfsListComponent', () => {
       refresh(new Summary());
       spyOn(nfsService, 'list').and.callFake(() => of(exports));
       fixture.detectChanges();
-
-      const req = httpTesting.expectOne('api/nfs-ganesha/daemon');
-      req.flush([]);
     });
 
     it('should gets all exports without tasks', () => {
index 70ff67eaf8c78b6cf1e365bfc3d0d5e476aa6b3b..d5d0c2639300c322820a59b3c539c97942828aa2 100644 (file)
@@ -118,11 +118,6 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr
         prop: 'cluster_id',
         flexGrow: 2
       },
-      {
-        name: $localize`Daemons`,
-        prop: 'daemons',
-        flexGrow: 2
-      },
       {
         name: $localize`Storage Backend`,
         prop: 'fsal',
@@ -136,32 +131,14 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr
       }
     ];
 
-    this.nfsService.daemon().subscribe(
-      (daemons: any) => {
-        const clusters = _(daemons)
-          .map((daemon) => daemon.cluster_id)
-          .uniq()
-          .value();
-
-        this.isDefaultCluster = clusters.length === 1 && clusters[0] === '_default_';
-        this.columns[2].isHidden = this.isDefaultCluster;
-        if (this.table) {
-          this.table.updateColumns();
-        }
-
-        this.taskListService.init(
-          () => this.nfsService.list(),
-          (resp) => this.prepareResponse(resp),
-          (exports) => (this.exports = exports),
-          () => this.onFetchError(),
-          this.taskFilter,
-          this.itemFilter,
-          this.builders
-        );
-      },
-      () => {
-        this.onFetchError();
-      }
+    this.taskListService.init(
+      () => this.nfsService.list(),
+      (resp) => this.prepareResponse(resp),
+      (exports) => (this.exports = exports),
+      () => this.onFetchError(),
+      this.taskFilter,
+      this.itemFilter,
+      this.builders
     );
   }
 
index 499692129318fcd9cf569d5e6a2e27bf84b074ca..445f2a5acc85ffec12fd63afca7177485ce7b8dd 100644 (file)
@@ -3,6 +3,7 @@ export class RgwDaemon {
   service_map_id: string;
   version: string;
   server_hostname: string;
+  realm_name: string;
   zonegroup_name: string;
   zone_name: string;
   default: boolean;
index e5aecb2eb57bf014ae8b76ca003fb5efd8514257..4cdb9935ddac1bfbda7598c727a42d6eeb14b495 100644 (file)
@@ -46,9 +46,9 @@
                   i18n>This field is required.</span>
             <span class="invalid-feedback"
                   *ngIf="bucketForm.showError('bid', frm, 'bucketNameInvalid')"
-                  i18n>The value is not valid.</span>
+                  i18n>Bucket names can only contain lowercase letters, numbers, periods and hyphens.</span>
             <span class="invalid-feedback"
-                  *ngIf="bucketForm.showError('bid', frm, 'bucketNameExists')"
+                  *ngIf="bucketForm.showError('bid', frm, 'bucketNameNotAllowed')"
                   i18n>The chosen name is already in use.</span>
             <span class="invalid-feedback"
                   *ngIf="bucketForm.showError('bid', frm, 'containsUpperCase')"
@@ -61,7 +61,7 @@
                   i18n>Bucket names cannot be formatted as IP address.</span>
             <span class="invalid-feedback"
                   *ngIf="bucketForm.showError('bid', frm, 'onlyLowerCaseAndNumbers')"
-                  i18n>Bucket names can only contain lowercase letters, numbers, and hyphens.</span>
+                  i18n>Bucket labels cannot be empty and can only contain lowercase letters, numbers and hyphens.</span>
             <span class="invalid-feedback"
                   *ngIf="bucketForm.showError('bid', frm, 'shouldBeInRange')"
                   i18n>Bucket names must be 3 to 63 characters long.</span>
index a2d8854adc873feea0cfdf03d753b3d84dff6381..704d7918465df24c8b9c4fbb928a625c72ffaca8 100644 (file)
@@ -1,12 +1,12 @@
 import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
 import { ReactiveFormsModule } from '@angular/forms';
 import { Router } from '@angular/router';
 import { RouterTestingModule } from '@angular/router/testing';
 
 import _ from 'lodash';
 import { ToastrModule } from 'ngx-toastr';
-import { of as observableOf, throwError } from 'rxjs';
+import { of as observableOf } from 'rxjs';
 
 import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
 import { RgwSiteService } from '~/app/shared/api/rgw-site.service';
@@ -54,92 +54,12 @@ describe('RgwBucketFormComponent', () => {
   });
 
   describe('bucketNameValidator', () => {
-    const testValidator = (name: string, valid: boolean, expectedError?: string) => {
-      rgwBucketServiceGetSpy.and.returnValue(throwError('foo'));
-      formHelper.setValue('bid', name, true);
-      tick();
-      if (valid) {
-        formHelper.expectValid('bid');
-      } else {
-        formHelper.expectError('bid', expectedError);
-      }
-    };
-
     it('should validate empty name', fakeAsync(() => {
       formHelper.expectErrorChange('bid', '', 'required', true);
     }));
+  });
 
-    it('bucket names cannot be formatted as IP address', fakeAsync(() => {
-      const testIPs = ['1.1.1.01', '001.1.1.01', '127.0.0.1'];
-      for (const ip of testIPs) {
-        testValidator(ip, false, 'ipAddress');
-      }
-    }));
-
-    it('bucket name must be >= 3 characters long (1/2)', fakeAsync(() => {
-      testValidator('ab', false, 'shouldBeInRange');
-    }));
-
-    it('bucket name must be >= 3 characters long (2/2)', fakeAsync(() => {
-      testValidator('abc', true);
-    }));
-
-    it('bucket name must be <= than 63 characters long (1/2)', fakeAsync(() => {
-      testValidator(_.repeat('a', 64), false, 'shouldBeInRange');
-    }));
-
-    it('bucket name must be <= than 63 characters long (2/2)', fakeAsync(() => {
-      testValidator(_.repeat('a', 63), true);
-    }));
-
-    it('bucket names must not contain uppercase characters or underscores (1/2)', fakeAsync(() => {
-      testValidator('iAmInvalid', false, 'containsUpperCase');
-    }));
-
-    it('bucket names can only contain lowercase letters, numbers, and hyphens', fakeAsync(() => {
-      testValidator('$$$', false, 'onlyLowerCaseAndNumbers');
-    }));
-
-    it('bucket names must not contain uppercase characters or underscores (2/2)', fakeAsync(() => {
-      testValidator('i_am_invalid', false, 'containsUpperCase');
-    }));
-
-    it('bucket names must start and end with letters or numbers', fakeAsync(() => {
-      testValidator('abcd-', false, 'lowerCaseOrNumber');
-    }));
-
-    it('bucket names with invalid labels (1/3)', fakeAsync(() => {
-      testValidator('abc.1def.Ghi2', false, 'containsUpperCase');
-    }));
-
-    it('bucket names with invalid labels (2/3)', fakeAsync(() => {
-      testValidator('abc.1_xy', false, 'containsUpperCase');
-    }));
-
-    it('bucket names with invalid labels (3/3)', fakeAsync(() => {
-      testValidator('abc.*def', false, 'lowerCaseOrNumber');
-    }));
-
-    it('bucket names must be a series of one or more labels and can contain lowercase letters, numbers, and hyphens (1/3)', fakeAsync(() => {
-      testValidator('xyz.abc', true);
-    }));
-
-    it('bucket names must be a series of one or more labels and can contain lowercase letters, numbers, and hyphens (2/3)', fakeAsync(() => {
-      testValidator('abc.1-def', true);
-    }));
-
-    it('bucket names must be a series of one or more labels and can contain lowercase letters, numbers, and hyphens (3/3)', fakeAsync(() => {
-      testValidator('abc.ghi2', true);
-    }));
-
-    it('bucket names must be unique', fakeAsync(() => {
-      testValidator('bucket-name-is-unique', true);
-    }));
-
-    it('bucket names must not contain spaces', fakeAsync(() => {
-      testValidator('bucket name  with   spaces', false, 'onlyLowerCaseAndNumbers');
-    }));
-
+  describe('zonegroup and placement targets', () => {
     it('should get zonegroup and placement targets', () => {
       const payload: Record<string, any> = {
         zonegroup: 'default',
index 80ff2c8538a5a24ed8ab1295f80a4e9ce05bfb41..1d5aede396eae52b8fd6ec2d22738b670550ccea 100644 (file)
@@ -1,10 +1,9 @@
 import { Component, OnInit } from '@angular/core';
-import { AbstractControl, AsyncValidatorFn, ValidationErrors, Validators } from '@angular/forms';
+import { Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 
 import _ from 'lodash';
-import { forkJoin, Observable, of as observableOf, timer as observableTimer } from 'rxjs';
-import { map, switchMapTo } from 'rxjs/operators';
+import { forkJoin } from 'rxjs';
 
 import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
 import { RgwSiteService } from '~/app/shared/api/rgw-site.service';
@@ -72,7 +71,13 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
     });
     this.bucketForm = this.formBuilder.group({
       id: [null],
-      bid: [null, [Validators.required], this.editing ? [] : [this.bucketNameValidator()]],
+      bid: [
+        null,
+        [Validators.required],
+        this.editing
+          ? []
+          : [CdValidators.bucketName(), CdValidators.bucketExistence(false, this.rgwBucketService)]
+      ],
       owner: [null, [Validators.required]],
       'placement-target': [null, this.editing ? [] : [Validators.required]],
       versioning: [null],
@@ -225,108 +230,6 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
     }
   }
 
-  /**
-   * Validate the bucket name. In general, bucket names should follow domain
-   * name constraints:
-   * - Bucket names must be unique.
-   * - Bucket names cannot be formatted as IP address.
-   * - Bucket names can be between 3 and 63 characters long.
-   * - Bucket names must not contain uppercase characters or underscores.
-   * - Bucket names must start with a lowercase letter or number.
-   * - Bucket names must be a series of one or more labels. Adjacent
-   *   labels are separated by a single period (.). Bucket names can
-   *   contain lowercase letters, numbers, and hyphens. Each label must
-   *   start and end with a lowercase letter or a number.
-   */
-  bucketNameValidator(): AsyncValidatorFn {
-    return (control: AbstractControl): Observable<ValidationErrors | null> => {
-      // Exit immediately if user has not interacted with the control yet
-      // or the control value is empty.
-      if (control.pristine || control.value === '') {
-        return observableOf(null);
-      }
-      const constraints = [];
-      let errorName: string;
-      // - Bucket names cannot be formatted as IP address.
-      constraints.push(() => {
-        const ipv4Rgx = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
-        const ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
-        const name = this.bucketForm.get('bid').value;
-        let notIP = true;
-        if (ipv4Rgx.test(name) || ipv6Rgx.test(name)) {
-          errorName = 'ipAddress';
-          notIP = false;
-        }
-        return notIP;
-      });
-      // - Bucket names can be between 3 and 63 characters long.
-      constraints.push((name: string) => {
-        if (!_.inRange(name.length, 3, 64)) {
-          errorName = 'shouldBeInRange';
-          return false;
-        }
-        return true;
-      });
-      // - Bucket names must not contain uppercase characters or underscores.
-      // - Bucket names must start with a lowercase letter or number.
-      // - Bucket names must be a series of one or more labels. Adjacent
-      //   labels are separated by a single period (.). Bucket names can
-      //   contain lowercase letters, numbers, and hyphens. Each label must
-      //   start and end with a lowercase letter or a number.
-      constraints.push((name: string) => {
-        const labels = _.split(name, '.');
-        return _.every(labels, (label) => {
-          // Bucket names must not contain uppercase characters or underscores.
-          if (label !== _.toLower(label) || label.includes('_')) {
-            errorName = 'containsUpperCase';
-            return false;
-          }
-          // Bucket names can contain lowercase letters, numbers, and hyphens.
-          if (!/^\S*$/.test(name) || !/[0-9a-z-]/.test(label)) {
-            errorName = 'onlyLowerCaseAndNumbers';
-            return false;
-          }
-          // Each label must start and end with a lowercase letter or a number.
-          return _.every([0, label.length - 1], (index) => {
-            errorName = 'lowerCaseOrNumber';
-            return /[a-z]/.test(label[index]) || _.isInteger(_.parseInt(label[index]));
-          });
-        });
-      });
-      if (!_.every(constraints, (func: Function) => func(control.value))) {
-        return observableTimer().pipe(
-          map(() => {
-            switch (errorName) {
-              case 'onlyLowerCaseAndNumbers':
-                return { onlyLowerCaseAndNumbers: true };
-              case 'shouldBeInRange':
-                return { shouldBeInRange: true };
-              case 'ipAddress':
-                return { ipAddress: true };
-              case 'containsUpperCase':
-                return { containsUpperCase: true };
-              case 'lowerCaseOrNumber':
-                return { lowerCaseOrNumber: true };
-              default:
-                return { bucketNameInvalid: true };
-            }
-          })
-        );
-      }
-      // - Bucket names must be unique.
-      return observableTimer().pipe(
-        switchMapTo(this.rgwBucketService.exists.call(this.rgwBucketService, control.value)),
-        map((resp: boolean) => {
-          if (!resp) {
-            return null;
-          } else {
-            return { bucketNameExists: true };
-          }
-        })
-      );
-    };
-  }
-
   areMfaCredentialsRequired() {
     return (
       this.isMfaDeleteEnabled !== this.isMfaDeleteAlreadyEnabled ||
index 515a0697dbc601faa59baf3c4f7365b059f0b191..479da864a44a6d4e8c049be00f7efa92a3b7f262 100644 (file)
@@ -140,7 +140,7 @@ export class RgwBucketListComponent extends ListWithDetails implements OnInit {
 
   getBucketList(context: CdTableFetchDataContext) {
     this.setTableRefreshTimeout();
-    this.rgwBucketService.list().subscribe(
+    this.rgwBucketService.list(true).subscribe(
       (resp: object[]) => {
         this.buckets = resp;
         this.transformBucketData();
index cb8ce571335d44203285a5dace19af7efd1a557d..96a7938942c3394e8d71a01906e819672556a73c 100644 (file)
@@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
 
 import { take } from 'rxjs/operators';
 
+import { RgwDaemon } from '~/app/ceph/rgw/models/rgw-daemon';
 import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
 import { RgwSiteService } from '~/app/shared/api/rgw-site.service';
 import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
@@ -68,7 +69,7 @@ export class RgwDaemonListComponent extends ListWithDetails implements OnInit {
 
   getDaemonList(context: CdTableFetchDataContext) {
     this.rgwDaemonService.daemons$.pipe(take(1)).subscribe(
-      (resp: object[]) => {
+      (resp: RgwDaemon[]) => {
         this.daemons = resp;
       },
       () => {
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/api-client.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/api-client.spec.ts
new file mode 100644 (file)
index 0000000..0d521a8
--- /dev/null
@@ -0,0 +1,11 @@
+import { ApiClient } from '~/app/shared/api/api-client';
+
+class MockApiClient extends ApiClient {}
+
+describe('ApiClient', () => {
+  const service = new MockApiClient();
+
+  it('should get the version header value', () => {
+    expect(service.getVersionHeaderValue(1, 2)).toBe('application/vnd.ceph.api.v1.2+json');
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/api-client.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/api-client.ts
new file mode 100644 (file)
index 0000000..06583eb
--- /dev/null
@@ -0,0 +1,5 @@
+export abstract class ApiClient {
+  getVersionHeaderValue(major: number, minor: number) {
+    return `application/vnd.ceph.api.v${major}.${minor}+json`;
+  }
+}
index a1f2497b5a709f5dc5f7e6ba0d9a034c724c68ed..2a7c580653c34e62604d717df2bdcbb43e019d4c 100644 (file)
@@ -7,6 +7,7 @@ import { map, mergeMap, toArray } from 'rxjs/operators';
 
 import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
 import { InventoryHost } from '~/app/ceph/cluster/inventory/inventory-host.model';
+import { ApiClient } from '~/app/shared/api/api-client';
 import { CdHelperClass } from '~/app/shared/classes/cd-helper.class';
 import { Daemon } from '../models/daemon.interface';
 import { CdDevice } from '../models/devices';
@@ -16,11 +17,13 @@ import { DeviceService } from '../services/device.service';
 @Injectable({
   providedIn: 'root'
 })
-export class HostService {
+export class HostService extends ApiClient {
   baseURL = 'api/host';
   baseUIURL = 'ui-api/host';
 
-  constructor(private http: HttpClient, private deviceService: DeviceService) {}
+  constructor(private http: HttpClient, private deviceService: DeviceService) {
+    super();
+  }
 
   list(facts: string): Observable<object[]> {
     return this.http.get<object[]>(this.baseURL, {
@@ -74,7 +77,7 @@ export class HostService {
         maintenance: maintenance,
         force: force
       },
-      { headers: { Accept: 'application/vnd.ceph.api.v0.1+json' } }
+      { headers: { Accept: this.getVersionHeaderValue(0, 1) } }
     );
   }
 
index 07f5a1890fbd38bce42a2bfc419fd78d995d14ee..c977f7ec5b514b3235d5cafae7c8acea36b362f9 100644 (file)
@@ -46,8 +46,8 @@ describe('NfsService', () => {
   });
 
   it('should call update', () => {
-    service.update('cluster_id', 'export_id', 'foo').subscribe();
-    const req = httpTesting.expectOne('api/nfs-ganesha/export/cluster_id/export_id');
+    service.update('cluster_id', 1, 'foo').subscribe();
+    const req = httpTesting.expectOne('api/nfs-ganesha/export/cluster_id/1');
     expect(req.request.body).toEqual('foo');
     expect(req.request.method).toBe('PUT');
   });
@@ -63,28 +63,4 @@ describe('NfsService', () => {
     const req = httpTesting.expectOne('ui-api/nfs-ganesha/lsdir/a?root_dir=foo_dir');
     expect(req.request.method).toBe('GET');
   });
-
-  it('should call buckets', () => {
-    service.buckets('user_foo').subscribe();
-    const req = httpTesting.expectOne('ui-api/nfs-ganesha/rgw/buckets?user_id=user_foo');
-    expect(req.request.method).toBe('GET');
-  });
-
-  it('should call daemon', () => {
-    service.daemon().subscribe();
-    const req = httpTesting.expectOne('api/nfs-ganesha/daemon');
-    expect(req.request.method).toBe('GET');
-  });
-
-  it('should call start', () => {
-    service.start('host_name').subscribe();
-    const req = httpTesting.expectOne('api/nfs-ganesha/service/host_name/start');
-    expect(req.request.method).toBe('PUT');
-  });
-
-  it('should call stop', () => {
-    service.stop('host_name').subscribe();
-    const req = httpTesting.expectOne('api/nfs-ganesha/service/host_name/stop');
-    expect(req.request.method).toBe('PUT');
-  });
 });
index 997faab9344e4b49347eb6cd472814ab381dfb73..88af7a68216f0034cd938ceb5a23559844d655b3 100644 (file)
@@ -1,10 +1,19 @@
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 
+import { Observable } from 'rxjs';
+
+import { NfsFSAbstractionLayer } from '~/app/ceph/nfs/models/nfs.fsal';
+import { ApiClient } from '~/app/shared/api/api-client';
+
+export interface Directory {
+  paths: string[];
+}
+
 @Injectable({
   providedIn: 'root'
 })
-export class NfsService {
+export class NfsService extends ApiClient {
   apiPath = 'api/nfs-ganesha';
   uiApiPath = 'ui-api/nfs-ganesha';
 
@@ -17,34 +26,30 @@ export class NfsService {
       value: 'RO',
       help: $localize`Allows only operations that do not modify the server`
     },
-    {
-      value: 'MDONLY',
-      help: $localize`Does not allow read or write operations, but allows any other operation`
-    },
-    {
-      value: 'MDONLY_RO',
-      help: $localize`Does not allow read, write, or any operation that modifies file attributes or directory content`
-    },
     {
       value: 'NONE',
       help: $localize`Allows no access at all`
     }
   ];
 
-  nfsFsal = [
+  nfsFsal: NfsFSAbstractionLayer[] = [
     {
       value: 'CEPH',
-      descr: $localize`CephFS`
+      descr: $localize`CephFS`,
+      disabled: false
     },
     {
       value: 'RGW',
-      descr: $localize`Object Gateway`
+      descr: $localize`Object Gateway`,
+      disabled: false
     }
   ];
 
   nfsSquash = ['no_root_squash', 'root_id_squash', 'root_squash', 'all_squash'];
 
-  constructor(private http: HttpClient) {}
+  constructor(private http: HttpClient) {
+    super();
+  }
 
   list() {
     return this.http.get(`${this.apiPath}/export`);
@@ -55,29 +60,34 @@ export class NfsService {
   }
 
   create(nfs: any) {
-    return this.http.post(`${this.apiPath}/export`, nfs, { observe: 'response' });
+    return this.http.post(`${this.apiPath}/export`, nfs, {
+      headers: { Accept: this.getVersionHeaderValue(2, 0) },
+      observe: 'response'
+    });
   }
 
-  update(clusterId: string, id: string, nfs: any) {
-    return this.http.put(`${this.apiPath}/export/${clusterId}/${id}`, nfs, { observe: 'response' });
+  update(clusterId: string, id: number, nfs: any) {
+    return this.http.put(`${this.apiPath}/export/${clusterId}/${id}`, nfs, {
+      headers: { Accept: this.getVersionHeaderValue(2, 0) },
+      observe: 'response'
+    });
   }
 
   delete(clusterId: string, exportId: string) {
     return this.http.delete(`${this.apiPath}/export/${clusterId}/${exportId}`, {
+      headers: { Accept: this.getVersionHeaderValue(2, 0) },
       observe: 'response'
     });
   }
 
-  lsDir(fs_name: string, root_dir: string) {
-    return this.http.get(`${this.uiApiPath}/lsdir/${fs_name}?root_dir=${root_dir}`);
-  }
-
-  buckets(user_id: string) {
-    return this.http.get(`${this.uiApiPath}/rgw/buckets?user_id=${user_id}`);
+  listClusters() {
+    return this.http.get(`${this.apiPath}/cluster`, {
+      headers: { Accept: this.getVersionHeaderValue(0, 1) }
+    });
   }
 
-  clients() {
-    return this.http.get(`${this.uiApiPath}/cephx/clients`);
+  lsDir(fs_name: string, root_dir: string): Observable<Directory> {
+    return this.http.get<Directory>(`${this.uiApiPath}/lsdir/${fs_name}?root_dir=${root_dir}`);
   }
 
   fsals() {
@@ -87,20 +97,4 @@ export class NfsService {
   filesystems() {
     return this.http.get(`${this.uiApiPath}/cephfs/filesystems`);
   }
-
-  daemon() {
-    return this.http.get(`${this.apiPath}/daemon`);
-  }
-
-  start(host_name: string) {
-    return this.http.put(`${this.apiPath}/service/${host_name}/start`, null, {
-      observe: 'response'
-    });
-  }
-
-  stop(host_name: string) {
-    return this.http.put(`${this.apiPath}/service/${host_name}/stop`, null, {
-      observe: 'response'
-    });
-  }
 }
index 6e3fbf660ca2f740f59e2023c59c1aa9b4853073..b22b67e349123c244dc7ad0f24c2f7c8c9fe4001 100644 (file)
@@ -29,7 +29,15 @@ describe('RgwBucketService', () => {
 
   it('should call list', () => {
     service.list().subscribe();
-    const req = httpTesting.expectOne(`api/rgw/bucket?${RgwHelper.DAEMON_QUERY_PARAM}&stats=true`);
+    const req = httpTesting.expectOne(`api/rgw/bucket?${RgwHelper.DAEMON_QUERY_PARAM}&stats=false`);
+    expect(req.request.method).toBe('GET');
+  });
+
+  it('should call list with stats and user id', () => {
+    service.list(true, 'test-name').subscribe();
+    const req = httpTesting.expectOne(
+      `api/rgw/bucket?${RgwHelper.DAEMON_QUERY_PARAM}&stats=true&uid=test-name`
+    );
     expect(req.request.method).toBe('GET');
   });
 
index 47126f186d19c4b8bf61198f9543ff589d8e6adc..fc88bfa7181649658c4c04005d7dd12ef750f7a7 100644 (file)
@@ -5,6 +5,7 @@ import _ from 'lodash';
 import { of as observableOf } from 'rxjs';
 import { catchError, mapTo } from 'rxjs/operators';
 
+import { ApiClient } from '~/app/shared/api/api-client';
 import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
 import { cdEncode } from '~/app/shared/decorators/cd-encode';
 
@@ -12,19 +13,27 @@ import { cdEncode } from '~/app/shared/decorators/cd-encode';
 @Injectable({
   providedIn: 'root'
 })
-export class RgwBucketService {
+export class RgwBucketService extends ApiClient {
   private url = 'api/rgw/bucket';
 
-  constructor(private http: HttpClient, private rgwDaemonService: RgwDaemonService) {}
+  constructor(private http: HttpClient, private rgwDaemonService: RgwDaemonService) {
+    super();
+  }
 
   /**
    * Get the list of buckets.
    * @return Observable<Object[]>
    */
-  list() {
+  list(stats: boolean = false, uid: string = '') {
     return this.rgwDaemonService.request((params: HttpParams) => {
-      params = params.append('stats', 'true');
-      return this.http.get(this.url, { params: params });
+      params = params.append('stats', stats.toString());
+      if (uid) {
+        params = params.append('uid', uid);
+      }
+      return this.http.get(this.url, {
+        headers: { Accept: this.getVersionHeaderValue(1, 1) },
+        params: params
+      });
     });
   }
 
index 545179dcf1abe44f7ccc7d8ebc90fb237e89226e..49589c83f4e071a23a75a77a621713435585ef6b 100644 (file)
@@ -1,6 +1,10 @@
 import { HttpClient, HttpParams } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 
+import { Observable } from 'rxjs';
+import { map, mergeMap } from 'rxjs/operators';
+
+import { RgwDaemon } from '~/app/ceph/rgw/models/rgw-daemon';
 import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
 import { cdEncode } from '~/app/shared/decorators/cd-encode';
 
@@ -21,4 +25,14 @@ export class RgwSiteService {
       return this.http.get(this.url, { params: params });
     });
   }
+
+  isDefaultRealm(): Observable<boolean> {
+    return this.get('default-realm').pipe(
+      mergeMap((defaultRealm: string) =>
+        this.rgwDaemonService.selectedDaemon$.pipe(
+          map((selectedDaemon: RgwDaemon) => selectedDaemon.realm_name === defaultRealm)
+        )
+      )
+    );
+  }
 }
index 557a179cc5aff63629d7f08a544a814e16648279..5cf90fdea5cb3b68af08cda544bc2391c686e749 100644 (file)
@@ -1,11 +1,24 @@
 import { fakeAsync, tick } from '@angular/core/testing';
 import { FormControl, Validators } from '@angular/forms';
 
+import _ from 'lodash';
 import { of as observableOf } from 'rxjs';
 
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
 import { FormHelper } from '~/testing/unit-test-helper';
-import { CdFormGroup } from './cd-form-group';
-import { CdValidators } from './cd-validators';
+
+let mockBucketExists = observableOf(true);
+jest.mock('~/app/shared/api/rgw-bucket.service', () => {
+  return {
+    RgwBucketService: jest.fn().mockImplementation(() => {
+      return {
+        exists: () => mockBucketExists
+      };
+    })
+  };
+});
 
 describe('CdValidators', () => {
   let formHelper: FormHelper;
@@ -755,4 +768,139 @@ describe('CdValidators', () => {
       });
     });
   });
+  describe('bucket', () => {
+    const testValidator = (name: string, valid: boolean, expectedError?: string) => {
+      formHelper.setValue('x', name, true);
+      tick();
+      if (valid) {
+        formHelper.expectValid('x');
+      } else {
+        formHelper.expectError('x', expectedError);
+      }
+    };
+
+    describe('bucketName', () => {
+      beforeEach(() => {
+        form = new CdFormGroup({
+          x: new FormControl('', null, CdValidators.bucketName())
+        });
+        formHelper = new FormHelper(form);
+      });
+
+      it('bucket name cannot be empty', fakeAsync(() => {
+        testValidator('', false, 'required');
+      }));
+
+      it('bucket names cannot be formatted as IP address', fakeAsync(() => {
+        const testIPs = ['1.1.1.01', '001.1.1.01', '127.0.0.1'];
+        for (const ip of testIPs) {
+          testValidator(ip, false, 'ipAddress');
+        }
+      }));
+
+      it('bucket name must be >= 3 characters long (1/2)', fakeAsync(() => {
+        testValidator('ab', false, 'shouldBeInRange');
+      }));
+
+      it('bucket name must be >= 3 characters long (2/2)', fakeAsync(() => {
+        testValidator('abc', true);
+      }));
+
+      it('bucket name must be <= than 63 characters long (1/2)', fakeAsync(() => {
+        testValidator(_.repeat('a', 64), false, 'shouldBeInRange');
+      }));
+
+      it('bucket name must be <= than 63 characters long (2/2)', fakeAsync(() => {
+        testValidator(_.repeat('a', 63), true);
+      }));
+
+      it('bucket names must not contain uppercase characters or underscores (1/2)', fakeAsync(() => {
+        testValidator('iAmInvalid', false, 'bucketNameInvalid');
+      }));
+
+      it('bucket names can only contain lowercase letters, numbers, periods and hyphens', fakeAsync(() => {
+        testValidator('bk@2', false, 'bucketNameInvalid');
+      }));
+
+      it('bucket names must not contain uppercase characters or underscores (2/2)', fakeAsync(() => {
+        testValidator('i_am_invalid', false, 'bucketNameInvalid');
+      }));
+
+      it('bucket names must start and end with letters or numbers', fakeAsync(() => {
+        testValidator('abcd-', false, 'lowerCaseOrNumber');
+      }));
+
+      it('bucket labels cannot be empty', fakeAsync(() => {
+        testValidator('bk.', false, 'onlyLowerCaseAndNumbers');
+      }));
+
+      it('bucket names with invalid labels (1/3)', fakeAsync(() => {
+        testValidator('abc.1def.Ghi2', false, 'bucketNameInvalid');
+      }));
+
+      it('bucket names with invalid labels (2/3)', fakeAsync(() => {
+        testValidator('abc.1_xy', false, 'bucketNameInvalid');
+      }));
+
+      it('bucket names with invalid labels (3/3)', fakeAsync(() => {
+        testValidator('abc.*def', false, 'bucketNameInvalid');
+      }));
+
+      it('bucket names must be a series of one or more labels and can contain lowercase letters, numbers, and hyphens (1/3)', fakeAsync(() => {
+        testValidator('xyz.abc', true);
+      }));
+
+      it('bucket names must be a series of one or more labels and can contain lowercase letters, numbers, and hyphens (2/3)', fakeAsync(() => {
+        testValidator('abc.1-def', true);
+      }));
+
+      it('bucket names must be a series of one or more labels and can contain lowercase letters, numbers, and hyphens (3/3)', fakeAsync(() => {
+        testValidator('abc.ghi2', true);
+      }));
+
+      it('bucket names must be unique', fakeAsync(() => {
+        testValidator('bucket-name-is-unique', true);
+      }));
+
+      it('bucket names must not contain spaces', fakeAsync(() => {
+        testValidator('bucket name  with   spaces', false, 'bucketNameInvalid');
+      }));
+    });
+
+    describe('bucketExistence', () => {
+      const rgwBucketService = new RgwBucketService(undefined, undefined);
+
+      beforeEach(() => {
+        form = new CdFormGroup({
+          x: new FormControl('', null, CdValidators.bucketExistence(false, rgwBucketService))
+        });
+        formHelper = new FormHelper(form);
+      });
+
+      it('bucket name cannot be empty', fakeAsync(() => {
+        testValidator('', false, 'required');
+      }));
+
+      it('bucket name should not exist but it does', fakeAsync(() => {
+        testValidator('testName', false, 'bucketNameNotAllowed');
+      }));
+
+      it('bucket name should not exist and it does not', fakeAsync(() => {
+        mockBucketExists = observableOf(false);
+        testValidator('testName', true);
+      }));
+
+      it('bucket name should exist but it does not', fakeAsync(() => {
+        form.get('x').setAsyncValidators(CdValidators.bucketExistence(true, rgwBucketService));
+        mockBucketExists = observableOf(false);
+        testValidator('testName', false, 'bucketNameNotAllowed');
+      }));
+
+      it('bucket name should exist and it does', fakeAsync(() => {
+        form.get('x').setAsyncValidators(CdValidators.bucketExistence(true, rgwBucketService));
+        mockBucketExists = observableOf(true);
+        testValidator('testName', true);
+      }));
+    });
+  });
 });
index 53fcc747b9f2acd97b7417056390806ec31f75d1..22371a50f71ecd0b85b65dd54842443c54a5ee65 100644 (file)
@@ -10,8 +10,9 @@ import _ from 'lodash';
 import { Observable, of as observableOf, timer as observableTimer } from 'rxjs';
 import { map, switchMapTo, take } from 'rxjs/operators';
 
-import { DimlessBinaryPipe } from '../pipes/dimless-binary.pipe';
-import { FormatterService } from '../services/formatter.service';
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { FormatterService } from '~/app/shared/services/formatter.service';
 
 export function isEmptyInputValue(value: any): boolean {
   return value == null || value.length === 0;
@@ -494,4 +495,118 @@ export class CdValidators {
       );
     };
   }
+
+  /**
+   * Validate the bucket name. In general, bucket names should follow domain
+   * name constraints:
+   * - Bucket names must be unique.
+   * - Bucket names cannot be formatted as IP address.
+   * - Bucket names can be between 3 and 63 characters long.
+   * - Bucket names must not contain uppercase characters or underscores.
+   * - Bucket names must start with a lowercase letter or number.
+   * - Bucket names must be a series of one or more labels. Adjacent
+   *   labels are separated by a single period (.). Bucket names can
+   *   contain lowercase letters, numbers, and hyphens. Each label must
+   *   start and end with a lowercase letter or a number.
+   */
+  static bucketName(): AsyncValidatorFn {
+    return (control: AbstractControl): Observable<ValidationErrors | null> => {
+      if (control.pristine || !control.value) {
+        return observableOf({ required: true });
+      }
+      const constraints = [];
+      let errorName: string;
+      // - Bucket names cannot be formatted as IP address.
+      constraints.push(() => {
+        const ipv4Rgx = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
+        const ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
+        const name = control.value;
+        let notIP = true;
+        if (ipv4Rgx.test(name) || ipv6Rgx.test(name)) {
+          errorName = 'ipAddress';
+          notIP = false;
+        }
+        return notIP;
+      });
+      // - Bucket names can be between 3 and 63 characters long.
+      constraints.push((name: string) => {
+        if (!_.inRange(name.length, 3, 64)) {
+          errorName = 'shouldBeInRange';
+          return false;
+        }
+        // Bucket names can only contain lowercase letters, numbers, periods and hyphens.
+        if (!/^[0-9a-z.-]+$/.test(control.value)) {
+          errorName = 'bucketNameInvalid';
+          return false;
+        }
+        return true;
+      });
+      // - Bucket names must not contain uppercase characters or underscores.
+      // - Bucket names must start with a lowercase letter or number.
+      // - Bucket names must be a series of one or more labels. Adjacent
+      //   labels are separated by a single period (.). Bucket names can
+      //   contain lowercase letters, numbers, and hyphens. Each label must
+      //   start and end with a lowercase letter or a number.
+      constraints.push((name: string) => {
+        const labels = _.split(name, '.');
+        return _.every(labels, (label) => {
+          // Bucket names must not contain uppercase characters or underscores.
+          if (label !== _.toLower(label) || label.includes('_')) {
+            errorName = 'containsUpperCase';
+            return false;
+          }
+          // Bucket labels can contain lowercase letters, numbers, and hyphens.
+          if (!/^[0-9a-z-]+$/.test(label)) {
+            errorName = 'onlyLowerCaseAndNumbers';
+            return false;
+          }
+          // Each label must start and end with a lowercase letter or a number.
+          return _.every([0, label.length - 1], (index) => {
+            errorName = 'lowerCaseOrNumber';
+            return /[a-z]/.test(label[index]) || _.isInteger(_.parseInt(label[index]));
+          });
+        });
+      });
+      if (!_.every(constraints, (func: Function) => func(control.value))) {
+        return observableOf(
+          (() => {
+            switch (errorName) {
+              case 'onlyLowerCaseAndNumbers':
+                return { onlyLowerCaseAndNumbers: true };
+              case 'shouldBeInRange':
+                return { shouldBeInRange: true };
+              case 'ipAddress':
+                return { ipAddress: true };
+              case 'containsUpperCase':
+                return { containsUpperCase: true };
+              case 'lowerCaseOrNumber':
+                return { lowerCaseOrNumber: true };
+              default:
+                return { bucketNameInvalid: true };
+            }
+          })()
+        );
+      }
+
+      return observableOf(null);
+    };
+  }
+
+  static bucketExistence(
+    requiredExistenceResult: boolean,
+    rgwBucketService: RgwBucketService
+  ): AsyncValidatorFn {
+    return (control: AbstractControl): Observable<ValidationErrors | null> => {
+      if (control.pristine || !control.value) {
+        return observableOf({ required: true });
+      }
+      return rgwBucketService
+        .exists(control.value)
+        .pipe(
+          map((existenceResult: boolean) =>
+            existenceResult === requiredExistenceResult ? null : { bucketNameNotAllowed: true }
+          )
+        );
+    };
+  }
 }
index 361754f243a5e74bcd892bb01a80b055fadf882f..7ae1f99eb691a27db27df84ee60b51a619a25467 100644 (file)
@@ -4960,37 +4960,14 @@ paths:
       summary: Get Monitor Details
       tags:
       - Monitor
-  /api/nfs-ganesha/daemon:
+  /api/nfs-ganesha/cluster:
     get:
       parameters: []
       responses:
         '200':
           content:
-            application/vnd.ceph.api.v1.0+json:
-              schema:
-                items:
-                  properties:
-                    cluster_id:
-                      description: Cluster identifier
-                      type: string
-                    cluster_type:
-                      description: Cluster type
-                      type: string
-                    daemon_id:
-                      description: Daemon identifier
-                      type: string
-                    desc:
-                      description: Status description
-                      type: string
-                    status:
-                      description: Status of daemon
-                      type: integer
-                  type: object
-                required:
-                - daemon_id
-                - cluster_id
-                - cluster_type
-                type: array
+            application/vnd.ceph.api.v0.1+json:
+              type: object
           description: OK
         '400':
           description: Operation exception. Please check the response body for details.
@@ -5003,7 +4980,6 @@ paths:
             trace.
       security:
       - jwt: []
-      summary: List NFS-Ganesha daemons information
       tags:
       - NFS-Ganesha
   /api/nfs-ganesha/export:
@@ -5043,31 +5019,23 @@ paths:
                     cluster_id:
                       description: Cluster identifier
                       type: string
-                    daemons:
-                      description: List of NFS Ganesha daemons identifiers
-                      items:
-                        type: string
-                      type: array
                     export_id:
                       description: Export ID
                       type: integer
                     fsal:
                       description: FSAL configuration
                       properties:
-                        filesystem:
-                          description: CephFS filesystem ID
+                        fs_name:
+                          description: CephFS filesystem name
                           type: string
                         name:
                           description: name of FSAL
                           type: string
-                        rgw_user_id:
-                          description: RGW user id
-                          type: string
                         sec_label_xattr:
                           description: Name of xattr for security label
                           type: string
                         user_id:
-                          description: CephX user id
+                          description: User id
                           type: string
                       required:
                       - name
@@ -5089,9 +5057,6 @@ paths:
                     squash:
                       description: Export squash policy
                       type: string
-                    tag:
-                      description: NFSv3 export tag
-                      type: string
                     transports:
                       description: List of transport types
                       items:
@@ -5102,9 +5067,7 @@ paths:
                 - export_id
                 - path
                 - cluster_id
-                - daemons
                 - pseudo
-                - tag
                 - access_type
                 - squash
                 - security_label
@@ -5162,29 +5125,18 @@ paths:
                 cluster_id:
                   description: Cluster identifier
                   type: string
-                daemons:
-                  description: List of NFS Ganesha daemons identifiers
-                  items:
-                    type: string
-                  type: array
                 fsal:
                   description: FSAL configuration
                   properties:
-                    filesystem:
-                      description: CephFS filesystem ID
+                    fs_name:
+                      description: CephFS filesystem name
                       type: string
                     name:
                       description: name of FSAL
                       type: string
-                    rgw_user_id:
-                      description: RGW user id
-                      type: string
                     sec_label_xattr:
                       description: Name of xattr for security label
                       type: string
-                    user_id:
-                      description: CephX user id
-                      type: string
                   required:
                   - name
                   type: object
@@ -5199,19 +5151,12 @@ paths:
                 pseudo:
                   description: Pseudo FS path
                   type: string
-                reload_daemons:
-                  default: true
-                  description: Trigger reload of NFS-Ganesha daemons configuration
-                  type: boolean
                 security_label:
                   description: Security label
                   type: string
                 squash:
                   description: Export squash policy
                   type: string
-                tag:
-                  description: NFSv3 export tag
-                  type: string
                 transports:
                   description: List of transport types
                   items:
@@ -5220,9 +5165,7 @@ paths:
               required:
               - path
               - cluster_id
-              - daemons
               - pseudo
-              - tag
               - access_type
               - squash
               - security_label
@@ -5234,7 +5177,7 @@ paths:
       responses:
         '201':
           content:
-            application/vnd.ceph.api.v1.0+json:
+            application/vnd.ceph.api.v2.0+json:
               schema:
                 properties:
                   access_type:
@@ -5264,31 +5207,23 @@ paths:
                   cluster_id:
                     description: Cluster identifier
                     type: string
-                  daemons:
-                    description: List of NFS Ganesha daemons identifiers
-                    items:
-                      type: string
-                    type: array
                   export_id:
                     description: Export ID
                     type: integer
                   fsal:
                     description: FSAL configuration
                     properties:
-                      filesystem:
-                        description: CephFS filesystem ID
+                      fs_name:
+                        description: CephFS filesystem name
                         type: string
                       name:
                         description: name of FSAL
                         type: string
-                      rgw_user_id:
-                        description: RGW user id
-                        type: string
                       sec_label_xattr:
                         description: Name of xattr for security label
                         type: string
                       user_id:
-                        description: CephX user id
+                        description: User id
                         type: string
                     required:
                     - name
@@ -5310,9 +5245,6 @@ paths:
                   squash:
                     description: Export squash policy
                     type: string
-                  tag:
-                    description: NFSv3 export tag
-                    type: string
                   transports:
                     description: List of transport types
                     items:
@@ -5322,9 +5254,7 @@ paths:
                 - export_id
                 - path
                 - cluster_id
-                - daemons
                 - pseudo
-                - tag
                 - access_type
                 - squash
                 - security_label
@@ -5336,7 +5266,7 @@ paths:
           description: Resource created.
         '202':
           content:
-            application/vnd.ceph.api.v1.0+json:
+            application/vnd.ceph.api.v2.0+json:
               type: object
           description: Operation is still executing. Please check the task queue.
         '400':
@@ -5368,21 +5298,15 @@ paths:
         required: true
         schema:
           type: integer
-      - default: true
-        description: Trigger reload of NFS-Ganesha daemons configuration
-        in: query
-        name: reload_daemons
-        schema:
-          type: boolean
       responses:
         '202':
           content:
-            application/vnd.ceph.api.v1.0+json:
+            application/vnd.ceph.api.v2.0+json:
               type: object
           description: Operation is still executing. Please check the task queue.
         '204':
           content:
-            application/vnd.ceph.api.v1.0+json:
+            application/vnd.ceph.api.v2.0+json:
               type: object
           description: Resource deleted.
         '400':
@@ -5412,7 +5336,7 @@ paths:
         name: export_id
         required: true
         schema:
-          type: integer
+          type: string
       responses:
         '200':
           content:
@@ -5446,31 +5370,23 @@ paths:
                   cluster_id:
                     description: Cluster identifier
                     type: string
-                  daemons:
-                    description: List of NFS Ganesha daemons identifiers
-                    items:
-                      type: string
-                    type: array
                   export_id:
                     description: Export ID
                     type: integer
                   fsal:
                     description: FSAL configuration
                     properties:
-                      filesystem:
-                        description: CephFS filesystem ID
+                      fs_name:
+                        description: CephFS filesystem name
                         type: string
                       name:
                         description: name of FSAL
                         type: string
-                      rgw_user_id:
-                        description: RGW user id
-                        type: string
                       sec_label_xattr:
                         description: Name of xattr for security label
                         type: string
                       user_id:
-                        description: CephX user id
+                        description: User id
                         type: string
                     required:
                     - name
@@ -5492,9 +5408,6 @@ paths:
                   squash:
                     description: Export squash policy
                     type: string
-                  tag:
-                    description: NFSv3 export tag
-                    type: string
                   transports:
                     description: List of transport types
                     items:
@@ -5504,9 +5417,7 @@ paths:
                 - export_id
                 - path
                 - cluster_id
-                - daemons
                 - pseudo
-                - tag
                 - access_type
                 - squash
                 - security_label
@@ -5573,29 +5484,18 @@ paths:
                     - squash
                     type: object
                   type: array
-                daemons:
-                  description: List of NFS Ganesha daemons identifiers
-                  items:
-                    type: string
-                  type: array
                 fsal:
                   description: FSAL configuration
                   properties:
-                    filesystem:
-                      description: CephFS filesystem ID
+                    fs_name:
+                      description: CephFS filesystem name
                       type: string
                     name:
                       description: name of FSAL
                       type: string
-                    rgw_user_id:
-                      description: RGW user id
-                      type: string
                     sec_label_xattr:
                       description: Name of xattr for security label
                       type: string
-                    user_id:
-                      description: CephX user id
-                      type: string
                   required:
                   - name
                   type: object
@@ -5610,19 +5510,12 @@ paths:
                 pseudo:
                   description: Pseudo FS path
                   type: string
-                reload_daemons:
-                  default: true
-                  description: Trigger reload of NFS-Ganesha daemons configuration
-                  type: boolean
                 security_label:
                   description: Security label
                   type: string
                 squash:
                   description: Export squash policy
                   type: string
-                tag:
-                  description: NFSv3 export tag
-                  type: string
                 transports:
                   description: List of transport types
                   items:
@@ -5630,9 +5523,7 @@ paths:
                   type: array
               required:
               - path
-              - daemons
               - pseudo
-              - tag
               - access_type
               - squash
               - security_label
@@ -5644,7 +5535,7 @@ paths:
       responses:
         '200':
           content:
-            application/vnd.ceph.api.v1.0+json:
+            application/vnd.ceph.api.v2.0+json:
               schema:
                 properties:
                   access_type:
@@ -5674,31 +5565,23 @@ paths:
                   cluster_id:
                     description: Cluster identifier
                     type: string
-                  daemons:
-                    description: List of NFS Ganesha daemons identifiers
-                    items:
-                      type: string
-                    type: array
                   export_id:
                     description: Export ID
                     type: integer
                   fsal:
                     description: FSAL configuration
                     properties:
-                      filesystem:
-                        description: CephFS filesystem ID
+                      fs_name:
+                        description: CephFS filesystem name
                         type: string
                       name:
                         description: name of FSAL
                         type: string
-                      rgw_user_id:
-                        description: RGW user id
-                        type: string
                       sec_label_xattr:
                         description: Name of xattr for security label
                         type: string
                       user_id:
-                        description: CephX user id
+                        description: User id
                         type: string
                     required:
                     - name
@@ -5720,9 +5603,6 @@ paths:
                   squash:
                     description: Export squash policy
                     type: string
-                  tag:
-                    description: NFSv3 export tag
-                    type: string
                   transports:
                     description: List of transport types
                     items:
@@ -5732,9 +5612,7 @@ paths:
                 - export_id
                 - path
                 - cluster_id
-                - daemons
                 - pseudo
-                - tag
                 - access_type
                 - squash
                 - security_label
@@ -5746,7 +5624,7 @@ paths:
           description: Resource updated.
         '202':
           content:
-            application/vnd.ceph.api.v1.0+json:
+            application/vnd.ceph.api.v2.0+json:
               type: object
           description: Operation is still executing. Please check the task queue.
         '400':
@@ -7506,10 +7384,15 @@ paths:
         name: daemon_name
         schema:
           type: string
+      - allowEmptyValue: true
+        in: query
+        name: uid
+        schema:
+          type: string
       responses:
         '200':
           content:
-            application/vnd.ceph.api.v1.0+json:
+            application/vnd.ceph.api.v1.1+json:
               type: object
           description: OK
         '400':
@@ -10408,7 +10291,7 @@ tags:
   name: MonPerfCounter
 - description: Get Monitor Details
   name: Monitor
-- description: NFS-Ganesha Management API
+- description: NFS-Ganesha Cluster Management API
   name: NFS-Ganesha
 - description: OSD management API
   name: OSD
index a45aa67ff917fb803a7880ea51a6fb8b1dcacd5f..6253566b71dbded310ea422f92833301f24db437 100644 (file)
@@ -9,7 +9,7 @@ from mgr_module import CLICommand, Option
 
 from ..controllers.cephfs import CephFS
 from ..controllers.iscsi import Iscsi, IscsiTarget
-from ..controllers.nfsganesha import NFSGanesha, NFSGaneshaExports, NFSGaneshaService
+from ..controllers.nfsganesha import NFSGanesha, NFSGaneshaExports
 from ..controllers.rbd import Rbd, RbdSnapshot, RbdTrash
 from ..controllers.rbd_mirroring import RbdMirroringPoolMode, \
     RbdMirroringPoolPeer, RbdMirroringSummary
@@ -42,7 +42,7 @@ Feature2Controller = {
     Features.ISCSI: [Iscsi, IscsiTarget],
     Features.CEPHFS: [CephFS],
     Features.RGW: [Rgw, RgwDaemon, RgwBucket, RgwUser],
-    Features.NFS: [NFSGanesha, NFSGaneshaService, NFSGaneshaExports],
+    Features.NFS: [NFSGanesha, NFSGaneshaExports],
 }
 
 
diff --git a/src/pybind/mgr/dashboard/services/cephx.py b/src/pybind/mgr/dashboard/services/cephx.py
deleted file mode 100644 (file)
index 60303ad..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .ceph_service import CephService
-
-
-class CephX(object):
-    @classmethod
-    def _entities_map(cls, entity_type=None):
-        auth_dump = CephService.send_command("mon", "auth list")
-        result = {}
-        for auth_entry in auth_dump['auth_dump']:
-            entity = auth_entry['entity']
-            if not entity_type or entity.startswith('{}.'.format(entity_type)):
-                entity_id = entity[entity.find('.')+1:]
-                result[entity_id] = auth_entry
-        return result
-
-    @classmethod
-    def _clients_map(cls):
-        return cls._entities_map("client")
-
-    @classmethod
-    def list_clients(cls):
-        return list(cls._clients_map())
-
-    @classmethod
-    def get_client_key(cls, client_id):
-        return cls._clients_map()[client_id]['key']
index e5f614ade228e6024cd542b6948ce1093567ff48..915f6d7fe17c4db57dc8e7be423b2042007a0b6f 100644 (file)
@@ -308,6 +308,9 @@ class RgwClient(RestClient):
     def _get_realms_info(self):  # type: () -> dict
         return json_str_to_object(self.proxy('GET', 'realm?list', None, None))
 
+    def _get_realm_info(self, realm_id: str) -> Dict[str, Any]:
+        return json_str_to_object(self.proxy('GET', f'realm?id={realm_id}', None, None))
+
     @staticmethod
     def _rgw_settings():
         return (Settings.RGW_API_ACCESS_KEY,
@@ -580,6 +583,16 @@ class RgwClient(RestClient):
 
         return []
 
+    def get_default_realm(self) -> str:
+        realms_info = self._get_realms_info()
+        if 'default_info' in realms_info and realms_info['default_info']:
+            realm_info = self._get_realm_info(realms_info['default_info'])
+            if 'name' in realm_info and realm_info['name']:
+                return realm_info['name']
+        raise DashboardException(msg='Default realm not found.',
+                                 http_status_code=404,
+                                 component='rgw')
+
     @RestClient.api_get('/{bucket_name}?versioning')
     def get_bucket_versioning(self, bucket_name, request=None):
         """
index df7521a4f50876f6f882f24c340a3887d7ab1860..f3338d7970bb61778f25894f01790766093beeb1 100644 (file)
@@ -5,10 +5,44 @@ from __future__ import absolute_import
 from unittest.mock import patch
 from urllib.parse import urlencode
 
-from ..controllers.nfsganesha import NFSGaneshaUi
+from ..controllers.nfsganesha import NFSGaneshaExports, NFSGaneshaUi
 from . import ControllerTestCase  # pylint: disable=no-name-in-module
 
 
+class NFSGaneshaExportsTest(ControllerTestCase):
+
+    def test_get_schema_export(self):
+        export = {
+            "export_id": 2,
+            "path": "bk1",
+            "cluster_id": "myc",
+            "pseudo": "/bk-ps",
+            "access_type": "RO",
+            "squash": "root_id_squash",
+            "security_label": False,
+            "protocols": [
+                4
+            ],
+            "transports": [
+                "TCP",
+                "UDP"
+            ],
+            "fsal": {
+                "name": "RGW",
+                "user_id": "dashboard",
+                "access_key_id": "UUU5YVVOQ2P5QTOPYNAN",
+                "secret_access_key": "7z87tMUUsHr67ZWx12pCbWkp9UyOldxhDuPY8tVN"
+            },
+            "clients": []
+        }
+        expected_schema_export = export
+        del expected_schema_export['fsal']['access_key_id']
+        del expected_schema_export['fsal']['secret_access_key']
+        self.assertDictEqual(
+            expected_schema_export,
+            NFSGaneshaExports._get_schema_export(export))  # pylint: disable=protected-access
+
+
 class NFSGaneshaUiControllerTest(ControllerTestCase):
     @classmethod
     def setup_server(cls):
index 2f71489f352252feac33f2202cc81ebdcb792201..c5fb94af3d1f629f1b6885cc2115dfe3f61ac803 100644 (file)
@@ -77,12 +77,14 @@ class RgwDaemonControllerTestCase(ControllerTestCase):
             {
                 'ceph_version': 'ceph version master (dev)',
                 'id': 'daemon1',
+                'realm_name': 'realm1',
                 'zonegroup_name': 'zg1',
                 'zone_name': 'zone1'
             },
             {
                 'ceph_version': 'ceph version master (dev)',
                 'id': 'daemon2',
+                'realm_name': 'realm2',
                 'zonegroup_name': 'zg2',
                 'zone_name': 'zone2'
             }]
@@ -93,6 +95,7 @@ class RgwDaemonControllerTestCase(ControllerTestCase):
             'service_map_id': '4832',
             'version': 'ceph version master (dev)',
             'server_hostname': 'host1',
+            'realm_name': 'realm1',
             'zonegroup_name': 'zg1',
             'zone_name': 'zone1', 'default': True
         },
@@ -101,6 +104,7 @@ class RgwDaemonControllerTestCase(ControllerTestCase):
             'service_map_id': '5356',
             'version': 'ceph version master (dev)',
             'server_hostname': 'host1',
+            'realm_name': 'realm2',
             'zonegroup_name': 'zg2',
             'zone_name': 'zone2',
             'default': False
index 6a786f0a5b7467f61604329d72fa13d4fe22d84c..7d84a149017c967ade46a1789d70c46b91db79ac 100644 (file)
@@ -79,6 +79,7 @@ PG_STATES = [
     "wait",
 ]
 
+NFS_GANESHA_SUPPORTED_FSALS = ['CEPH', 'RGW']
 NFS_POOL_NAME = '.nfs'
 
 
index 9902ee308a904784c11acbaad31566c71a8a6959..e47c6bb0acaeec67d880876144b3ea0220e7c833 100644 (file)
@@ -149,21 +149,6 @@ class NFSCluster:
         except Exception as e:
             return exception_handler(e, "Failed to list NFS Cluster")
 
-    # FIXME: Remove this method. It was added for dashboard integration with mgr/nfs module.
-    def list_daemons(self):
-        completion = self.mgr.list_daemons(daemon_type='nfs')
-        # Here completion.result is a list DaemonDescription objects
-        daemons = orchestrator.raise_if_exception(completion)
-        return [
-            {
-                'cluster_id': instance.service_id(),
-                'daemon_id': instance.daemon_id,
-                'cluster_type': 'orchestrator',
-                'status': instance.status,
-                'status_desc': instance.status_desc
-            } for instance in daemons
-        ]
-
     def _show_nfs_cluster_info(self, cluster_id: str) -> Dict[str, Any]:
         completion = self.mgr.list_daemons(daemon_type='nfs')
         # Here completion.result is a list DaemonDescription objects
@@ -213,7 +198,6 @@ class NFSCluster:
 
     def show_nfs_cluster_info(self, cluster_id: Optional[str] = None) -> Tuple[int, str, str]:
         try:
-            cluster_ls = []
             info_res = {}
             if cluster_id:
                 cluster_ls = [cluster_id]
index 9e24203f8dd8e7064353c32424114b908ebad108..f48561ed22b3bf70e416a0e9f7c086f6379e27bf 100644 (file)
@@ -7,10 +7,9 @@ from os.path import normpath
 
 from rados import TimedOut, ObjectNotFound
 
-from mgr_module import NFS_POOL_NAME as POOL_NAME
+from mgr_module import NFS_POOL_NAME as POOL_NAME, NFS_GANESHA_SUPPORTED_FSALS
 
-from .export_utils import GaneshaConfParser, Export, RawBlock, CephFSFSAL, RGWFSAL, \
-    NFS_GANESHA_SUPPORTED_FSALS
+from .export_utils import GaneshaConfParser, Export, RawBlock, CephFSFSAL, RGWFSAL
 from .exception import NFSException, NFSInvalidOperation, FSNotFound, \
     ClusterNotFound
 from .utils import available_clusters, check_fs, restart_nfs_service
@@ -379,7 +378,7 @@ class ExportMgr:
                 raise NFSException(f"Failed to delete exports: {err} and {ret}")
         log.info("All exports successfully deleted for cluster id: %s", cluster_id)
 
-    def list_all_exports(self):
+    def list_all_exports(self) -> List[Dict[str, Any]]:
         r = []
         for cluster_id, ls in self.exports.items():
             r.extend([e.to_dict() for e in ls])
@@ -408,6 +407,7 @@ class ExportMgr:
         if export:
             return export.to_dict()
         log.warning(f"No {pseudo_path} export to show for {cluster_id}")
+        return None
 
     @export_cluster_checker
     def get_export(
@@ -428,7 +428,7 @@ class ExportMgr:
             self,
             cluster_id: str,
             export_id: int
-    ) -> Dict[Any, Any]:
+    ) -> Optional[Dict[str, Any]]:
         export = self._fetch_export_id(cluster_id, export_id)
         return export.to_dict() if export else None
 
@@ -571,7 +571,7 @@ class ExportMgr:
                 raise NFSInvalidOperation(f"export FSAL user_id must be '{user_id}'")
         else:
             raise NFSInvalidOperation(f"NFS Ganesha supported FSALs are {NFS_GANESHA_SUPPORTED_FSALS}."
-                                       "Export must specify any one of it.")
+                                      "Export must specify any one of it.")
 
         ex_dict["fsal"] = fsal
         ex_dict["cluster_id"] = cluster_id
index 6967108fae4c8ad5a0695f18e0623bc1abcca07b..8733545362eaeb0d77b42a22b14a248953e6f1d1 100644 (file)
@@ -1,13 +1,14 @@
 from typing import cast, List, Dict, Any, Optional, TYPE_CHECKING
 from os.path import isabs
 
+from mgr_module import NFS_GANESHA_SUPPORTED_FSALS
+
 from .exception import NFSInvalidOperation, FSNotFound
 from .utils import check_fs
 
 if TYPE_CHECKING:
     from nfs.module import Module
 
-NFS_GANESHA_SUPPORTED_FSALS = ['CEPH', 'RGW']
 
 class RawBlock():
     def __init__(self, block_name: str, blocks: List['RawBlock'] = [], values: Dict[str, Any] = {}):
index 8fc8558d098404e5483a26712a1a263811a908aa..158e966fed443bbf1afb77bb1a382c66c19e6aa5 100644 (file)
@@ -131,28 +131,17 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule):
         """Reset NFS-Ganesha Config to default"""
         return self.nfs.reset_nfs_cluster_config(cluster_id=cluster_id)
 
-    def fetch_nfs_export_obj(self):
+    def fetch_nfs_export_obj(self) -> ExportMgr:
         return self.export_mgr
 
     def export_ls(self) -> List[Dict[Any, Any]]:
         return self.export_mgr.list_all_exports()
 
-    def export_get(self, cluster_id: str, export_id: int) -> Dict[Any, Any]:
+    def export_get(self, cluster_id: str, export_id: int) -> Optional[Dict[str, Any]]:
         return self.export_mgr.get_export_by_id(cluster_id, export_id)
 
     def export_rm(self, cluster_id: str, pseudo: str) -> None:
         self.export_mgr.delete_export(cluster_id=cluster_id, pseudo_path=pseudo)
 
-    def daemon_ls(self) -> List[Dict[Any, Any]]:
-        return self.nfs.list_daemons()
-
-    # Remove this method after fixing attribute error
     def cluster_ls(self) -> List[str]:
-        return [
-            {
-                'pool': NFS_POOL_NAME,
-                'namespace': cluster_id,
-                'type': 'orchestrator',
-                'daemon_conf': None,
-            } for cluster_id in available_clusters()
-        ]
+        return available_clusters(self)
index 04394fedf7bbefa15e512fcb01621dc2cdfce8d4..c62ad2e90c2f6963b6e61660f650df629085e5a9 100644 (file)
@@ -1,7 +1,7 @@
 # flake8: noqa
 import json
 import pytest
-from typing import Optional, Tuple, Iterator, List, Any, Dict
+from typing import Optional, Tuple, Iterator, List, Any
 
 from contextlib import contextmanager
 from unittest import mock
@@ -324,9 +324,9 @@ NFS_CORE_PARAM {
         assert export.protocols == [4, 3]
         assert set(export.transports) == {"TCP", "UDP"}
         assert export.fsal.name == "RGW"
-        # assert export.fsal.rgw_user_id == "testuser"  # probably correct value
-        # assert export.fsal.access_key == "access_key"  # probably correct value
-        # assert export.fsal.secret_key == "secret_key"  # probably correct value
+        assert export.fsal.user_id == "nfs.foo.bucket"
+        assert export.fsal.access_key_id == "the_access_key"
+        assert export.fsal.secret_access_key == "the_secret_key"
         assert len(export.clients) == 0
         assert export.cluster_id == 'foo'
 
@@ -474,7 +474,9 @@ NFS_CORE_PARAM {
             'clients': [],
             'fsal': {
                 'name': 'RGW',
-                'rgw_user_id': 'rgw.foo.bucket'
+                'user_id': 'rgw.foo.bucket',
+                'access_key_id': 'the_access_key',
+                'secret_access_key': 'the_secret_key'
             }
         })
 
@@ -486,9 +488,9 @@ NFS_CORE_PARAM {
         assert set(export.protocols) == {4, 3}
         assert set(export.transports) == {"TCP", "UDP"}
         assert export.fsal.name == "RGW"
-#        assert export.fsal.rgw_user_id == "testuser"
-#        assert export.fsal.access_key is None
-#        assert export.fsal.secret_key is None
+        assert export.fsal.user_id == "rgw.foo.bucket"
+        assert export.fsal.access_key_id == "the_access_key"
+        assert export.fsal.secret_access_key == "the_secret_key"
         assert len(export.clients) == 0
         assert export.cluster_id == self.cluster_id
 
index 86fa87c8b82cbee7bc0762e3551b39a020b93f58..f71349b3d5e30ecd07760939d7ef2bb6ffc8d75b 100755 (executable)
@@ -1204,10 +1204,6 @@ EOF
 
         echo "$test_user ganesha daemon $name started on port: $port"
     done
-
-    if $with_mgr_dashboard; then
-        ceph_adm dashboard set-ganesha-clusters-rados-pool-namespace "$cluster_id:$pool_name/$cluster_id"
-    fi
 }
 
 if [ "$debug" -eq 0 ]; then