]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: RGW user accounts CRUD api's 60940/head
authorNaman Munet <namanmunet@li-ff83bccc-26af-11b2-a85c-a4b04bfb1003.ibm.com>
Wed, 4 Dec 2024 09:38:42 +0000 (15:08 +0530)
committerNaman Munet <naman.munet@ibm.com>
Tue, 17 Dec 2024 05:23:51 +0000 (10:53 +0530)
Fixes: https://tracker.ceph.com/issues/69110
PR Changes:
Added CRUD & managing quota endpoints for RGW user accounts management with the unit-tests

Signed-off-by: Naman Munet <namanmunet@li-ff83bccc-26af-11b2-a85c-a4b04bfb1003.ibm.com>
src/pybind/mgr/dashboard/controllers/rgw_iam.py [new file with mode: 0644]
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rgw_iam.py
src/pybind/mgr/dashboard/tests/test_rgw_iam.py [new file with mode: 0644]

diff --git a/src/pybind/mgr/dashboard/controllers/rgw_iam.py b/src/pybind/mgr/dashboard/controllers/rgw_iam.py
new file mode 100644 (file)
index 0000000..458bbbb
--- /dev/null
@@ -0,0 +1,52 @@
+from typing import Optional
+
+from ..security import Scope
+from ..services.rgw_iam import RgwAccounts
+from ..tools import str_to_bool
+from . import APIDoc, APIRouter, EndpointDoc, RESTController, allow_empty_body
+
+
+@APIRouter('rgw/accounts', Scope.RGW)
+@APIDoc("RGW User Accounts API", "RgwUserAccounts")
+class RgwUserAccountsController(RESTController):
+
+    @allow_empty_body
+    def create(self, account_name: Optional[str] = None,
+               account_id: Optional[str] = None, email: Optional[str] = None):
+        return RgwAccounts.create_account(account_name, account_id, email)
+
+    def list(self, detailed: bool = False):
+        detailed = str_to_bool(detailed)
+        return RgwAccounts.get_accounts(detailed)
+
+    @EndpointDoc("Get RGW Account by id",
+                 parameters={'account_id': (str, 'Account id')})
+    def get(self, account_id: str):
+        return RgwAccounts.get_account(account_id)
+
+    @EndpointDoc("Delete RGW Account",
+                 parameters={'account_id': (str, 'Account id')})
+    def delete(self, account_id):
+        return RgwAccounts.delete_account(account_id)
+
+    @EndpointDoc("Update RGW account info",
+                 parameters={'account_id': (str, 'Account id')})
+    @allow_empty_body
+    def set(self, account_id: str, account_name: Optional[str] = None,
+            email: Optional[str] = None):
+        return RgwAccounts.modify_account(account_id, account_name, email)
+
+    @EndpointDoc("Set RGW Account/Bucket quota",
+                 parameters={'account_id': (str, 'Account id'),
+                             'max_size': (str, 'Max size')})
+    @RESTController.Resource(method='PUT', path='/quota')
+    @allow_empty_body
+    def set_quota(self, quota_type: str, account_id: str, max_size: str, max_objects: str):
+        return RgwAccounts.set_quota(quota_type, account_id, max_size, max_objects)
+
+    @EndpointDoc("Enable/Disable RGW Account/Bucket quota",
+                 parameters={'account_id': (str, 'Account id')})
+    @RESTController.Resource(method='PUT', path='/quota/status')
+    @allow_empty_body
+    def set_quota_status(self, quota_type: str, account_id: str, quota_status: str):
+        return RgwAccounts.set_quota_status(quota_type, account_id, quota_status)
index b464344e27a2f76cc7a590529b25704ccd9d01ac..2e635c94833f6ea10a75df21c298bb7474af3d84 100644 (file)
@@ -10790,6 +10790,274 @@ paths:
       - jwt: []
       tags:
       - Prometheus
+  /api/rgw/accounts:
+    get:
+      parameters:
+      - default: false
+        in: query
+        name: detailed
+        schema:
+          type: boolean
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: OK
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      tags:
+      - RgwUserAccounts
+    post:
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                account_id:
+                  type: integer
+                account_name:
+                  type: integer
+                email:
+                  type: string
+              type: object
+      responses:
+        '201':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource created.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      tags:
+      - RgwUserAccounts
+  /api/rgw/accounts/{account_id}:
+    delete:
+      parameters:
+      - description: Account id
+        in: path
+        name: account_id
+        required: true
+        schema:
+          type: string
+      responses:
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '204':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource deleted.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Delete RGW Account
+      tags:
+      - RgwUserAccounts
+    get:
+      parameters:
+      - description: Account id
+        in: path
+        name: account_id
+        required: true
+        schema:
+          type: string
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: OK
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Get RGW Account by id
+      tags:
+      - RgwUserAccounts
+    put:
+      parameters:
+      - description: Account id
+        in: path
+        name: account_id
+        required: true
+        schema:
+          type: string
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                account_name:
+                  type: integer
+                email:
+                  type: string
+              type: object
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource updated.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Update RGW account info
+      tags:
+      - RgwUserAccounts
+  /api/rgw/accounts/{account_id}/quota:
+    put:
+      parameters:
+      - description: Account id
+        in: path
+        name: account_id
+        required: true
+        schema:
+          type: string
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                max_objects:
+                  type: string
+                max_size:
+                  description: Max size
+                  type: string
+                quota_type:
+                  type: string
+              required:
+              - quota_type
+              - max_size
+              - max_objects
+              type: object
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource updated.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Set RGW Account/Bucket quota
+      tags:
+      - RgwUserAccounts
+  /api/rgw/accounts/{account_id}/quota/status:
+    put:
+      parameters:
+      - description: Account id
+        in: path
+        name: account_id
+        required: true
+        schema:
+          type: string
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                quota_status:
+                  type: string
+                quota_type:
+                  type: string
+              required:
+              - quota_type
+              - quota_status
+              type: object
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource updated.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Enable/Disable RGW Account/Bucket quota
+      tags:
+      - RgwUserAccounts
   /api/rgw/bucket:
     get:
       parameters:
@@ -15472,6 +15740,8 @@ tags:
   name: RgwSite
 - description: RGW User Management API
   name: RgwUser
+- description: RGW User Accounts API
+  name: RgwUserAccounts
 - description: '*No description available*'
   name: RgwZone
 - description: '*No description available*'
index dbf00df25e0bbcc641ce4f8ca51080164507d977..5f490323441ace6fb4bb6bcf04b5472f60b61384 100644 (file)
@@ -1,12 +1,13 @@
 from subprocess import SubprocessError
-from typing import List
+from typing import List, Optional
 
 from .. import mgr
 from ..exceptions import DashboardException
 
 
 class RgwAccounts:
-    def send_rgw_cmd(self, command: List[str]):
+    @classmethod
+    def send_rgw_cmd(cls, command: List[str]):
         try:
             exit_code, out, err = mgr.send_rgwadmin_command(command)
 
@@ -19,6 +20,78 @@ class RgwAccounts:
         except SubprocessError as e:
             raise DashboardException(e, component='rgw')
 
-    def get_accounts(self):
+    @classmethod
+    def get_accounts(cls, detailed: bool = False):
+        """
+        Query account Id's, optionally returning full details.
+
+        :param detailed: Boolean to indicate if full account details are required.
+        """
         get_accounts_cmd = ['account', 'list']
-        return self.send_rgw_cmd(get_accounts_cmd)
+        account_list = cls.send_rgw_cmd(get_accounts_cmd)
+        detailed_account_list = []
+        if detailed:
+            for account in account_list:
+                detailed_account_list.append(cls.get_account(account))
+            return detailed_account_list
+        return account_list
+
+    @classmethod
+    def get_account(cls, account_id: str):
+        get_account_cmd = ['account', 'get', '--account-id', account_id]
+        return cls.send_rgw_cmd(get_account_cmd)
+
+    @classmethod
+    def create_account(cls, account_name: Optional[str] = None,
+                       account_id: Optional[str] = None, email: Optional[str] = None):
+        create_accounts_cmd = ['account', 'create']
+
+        if account_name:
+            create_accounts_cmd += ['--account-name', account_name]
+
+        if account_id:
+            create_accounts_cmd += ['--account_id', account_id]
+
+        if email:
+            create_accounts_cmd += ['--email', email]
+
+        return cls.send_rgw_cmd(create_accounts_cmd)
+
+    @classmethod
+    def modify_account(cls, account_id: str, account_name: Optional[str] = None,
+                       email: Optional[str] = None):
+        modify_accounts_cmd = ['account', 'modify', '--account-id', account_id]
+
+        if account_name:
+            modify_accounts_cmd += ['--account-name', account_name]
+
+        if email:
+            modify_accounts_cmd += ['--email', email]
+
+        return cls.send_rgw_cmd(modify_accounts_cmd)
+
+    @classmethod
+    def delete_account(cls, account_id: str):
+        modify_accounts_cmd = ['account', 'rm', '--account-id', account_id]
+
+        return cls.send_rgw_cmd(modify_accounts_cmd)
+
+    @classmethod
+    def get_account_stats(cls, account_id: str):
+        account_stats_cmd = ['account', 'stats', '--account-id', account_id]
+
+        return cls.send_rgw_cmd(account_stats_cmd)
+
+    @classmethod
+    def set_quota(cls, quota_type: str, account_id: str, max_size: str, max_objects: str):
+        set_quota_cmd = ['quota', 'set', '--quota-scope', quota_type, '--account-id', account_id,
+                         '--max-size', max_size, '--max-objects', max_objects]
+
+        return cls.send_rgw_cmd(set_quota_cmd)
+
+    @classmethod
+    def set_quota_status(cls, quota_type: str, account_id: str, quota_status: str):
+        set_quota_status_cmd = ['quota', quota_status, '--quota-scope', quota_type,
+                                '--account-id', account_id]
+
+        return cls.send_rgw_cmd(set_quota_status_cmd)
diff --git a/src/pybind/mgr/dashboard/tests/test_rgw_iam.py b/src/pybind/mgr/dashboard/tests/test_rgw_iam.py
new file mode 100644 (file)
index 0000000..133b5a0
--- /dev/null
@@ -0,0 +1,292 @@
+from unittest import TestCase
+from unittest.mock import patch
+
+from ..controllers.rgw_iam import RgwUserAccountsController
+from ..services.rgw_iam import RgwAccounts
+
+
+class TestRgwUserAccountsController(TestCase):
+
+    @patch.object(RgwAccounts, 'create_account')
+    def test_create_account(self, mock_create_account):
+        mockReturnVal = {
+            "id": "RGW18661471562806836",
+            "tenant": "",
+            "name": "",
+            "email": "",
+            "quota": {
+                "enabled": False,
+                "check_on_raw": False,
+                "max_size": -1,
+                "max_size_kb": 0,
+                "max_objects": -1
+            },
+            "bucket_quota": {
+                "enabled": False,
+                "check_on_raw": False,
+                "max_size": -1,
+                "max_size_kb": 0,
+                "max_objects": -1
+            },
+            "max_users": 1000,
+            "max_roles": 1000,
+            "max_groups": 1000,
+            "max_buckets": 1000,
+            "max_access_keys": 4
+        }
+
+        # Mock the return value of the create_account method
+        mock_create_account.return_value = mockReturnVal
+
+        controller = RgwUserAccountsController()
+        result = controller.create(account_name='test_account', account_id='RGW18661471562806836',
+                                   email='test@example.com')
+
+        # Check if the account creation method was called with the correct parameters
+        mock_create_account.assert_called_with('test_account', 'RGW18661471562806836',
+                                               'test@example.com')
+        # Check the returned result
+        self.assertEqual(result, mockReturnVal)
+
+    @patch.object(RgwAccounts, 'get_accounts')
+    def test_list_accounts(self, mock_get_accounts):
+        mock_return_value = [
+            "RGW22222222222222222",
+            "RGW59378973811515857",
+            "RGW11111111111111111"
+        ]
+
+        mock_get_accounts.return_value = mock_return_value
+
+        controller = RgwUserAccountsController()
+        result = controller.list(detailed=False)
+
+        mock_get_accounts.assert_called_with(False)
+
+        self.assertEqual(result, mock_return_value)
+
+    @patch.object(RgwAccounts, 'get_accounts')
+    def test_list_accounts_with_details(self, mock_get_accounts):
+        mock_return_value = [
+            {
+                "id": "RGW22222222222222222",
+                "tenant": "",
+                "name": "Account2",
+                "email": "account2@ceph.com",
+                "quota": {
+                    "enabled": False,
+                    "check_on_raw": False,
+                    "max_size": -1,
+                    "max_size_kb": 0,
+                    "max_objects": -1
+                },
+                "bucket_quota": {
+                    "enabled": False,
+                    "check_on_raw": False,
+                    "max_size": -1,
+                    "max_size_kb": 0,
+                    "max_objects": -1
+                },
+                "max_users": 1000,
+                "max_roles": 1000,
+                "max_groups": 1000,
+                "max_buckets": 1000,
+                "max_access_keys": 4
+            },
+            {
+                "id": "RGW11111111111111111",
+                "tenant": "",
+                "name": "Account1",
+                "email": "account1@ceph.com",
+                "quota": {
+                    "enabled": False,
+                    "check_on_raw": False,
+                    "max_size": -1,
+                    "max_size_kb": 0,
+                    "max_objects": -1
+                },
+                "bucket_quota": {
+                    "enabled": False,
+                    "check_on_raw": False,
+                    "max_size": -1,
+                    "max_size_kb": 0,
+                    "max_objects": -1
+                },
+                "max_users": 1000,
+                "max_roles": 1000,
+                "max_groups": 1000,
+                "max_buckets": 1000,
+                "max_access_keys": 4
+            }
+        ]
+
+        mock_get_accounts.return_value = mock_return_value
+
+        controller = RgwUserAccountsController()
+        result = controller.list(detailed=True)
+
+        mock_get_accounts.assert_called_with(True)
+
+        self.assertEqual(result, mock_return_value)
+
+    @patch.object(RgwAccounts, 'get_account')
+    def test_get_account(self, mock_get_account):
+        mock_return_value = {
+            "id": "RGW22222222222222222",
+            "tenant": "",
+            "name": "Account2",
+            "email": "account2@ceph.com",
+            "quota": {
+                "enabled": False,
+                "check_on_raw": False,
+                "max_size": -1,
+                "max_size_kb": 0,
+                "max_objects": -1
+            },
+            "bucket_quota": {
+                "enabled": False,
+                "check_on_raw": False,
+                "max_size": -1,
+                "max_size_kb": 0,
+                "max_objects": -1
+            },
+            "max_users": 1000,
+            "max_roles": 1000,
+            "max_groups": 1000,
+            "max_buckets": 1000,
+            "max_access_keys": 4
+        }
+        mock_get_account.return_value = mock_return_value
+
+        controller = RgwUserAccountsController()
+        result = controller.get(account_id='RGW22222222222222222')
+
+        mock_get_account.assert_called_with('RGW22222222222222222')
+
+        self.assertEqual(result, mock_return_value)
+
+    @patch.object(RgwAccounts, 'delete_account')
+    def test_delete_account(self, mock_delete_account):
+        mock_delete_account.return_value = None
+
+        controller = RgwUserAccountsController()
+        result = controller.delete(account_id='RGW59378973811515857')
+
+        mock_delete_account.assert_called_with('RGW59378973811515857')
+
+        self.assertEqual(result, None)
+
+    @patch.object(RgwAccounts, 'modify_account')
+    def test_set_account_name(self, mock_modify_account):
+        mock_return_value = mock_return_value = {
+            "id": "RGW59378973811515857",
+            "tenant": "",
+            "name": "new_account_name",
+            "email": "new_email@example.com",
+            "quota": {
+                "enabled": False,
+                "check_on_raw": False,
+                "max_size": -1,
+                "max_size_kb": 0,
+                "max_objects": -1
+            },
+            "bucket_quota": {
+                "enabled": False,
+                "check_on_raw": False,
+                "max_size": -1,
+                "max_size_kb": 0,
+                "max_objects": -1
+            },
+            "max_users": 1000,
+            "max_roles": 1000,
+            "max_groups": 1000,
+            "max_buckets": 1000,
+            "max_access_keys": 4
+        }
+        mock_modify_account.return_value = mock_return_value
+
+        controller = RgwUserAccountsController()
+        result = controller.set(account_id='RGW59378973811515857', account_name='new_account_name',
+                                email='new_email@example.com')
+
+        mock_modify_account.assert_called_with('RGW59378973811515857', 'new_account_name',
+                                               'new_email@example.com')
+
+        self.assertEqual(result, mock_return_value)
+
+    @patch.object(RgwAccounts, 'set_quota')
+    def test_set_quota(self, mock_set_quota):
+        mock_return_value = {
+            "id": "RGW11111111111111111",
+            "tenant": "",
+            "name": "Account1",
+            "email": "account1@ceph.com",
+            "quota": {
+                "enabled": False,
+                "check_on_raw": False,
+                "max_size": 10737418240,
+                "max_size_kb": 10485760,
+                "max_objects": 1000000
+            },
+            "bucket_quota": {
+                "enabled": False,
+                "check_on_raw": False,
+                "max_size": -1,
+                "max_size_kb": 0,
+                "max_objects": 1000000
+            },
+            "max_users": 1000,
+            "max_roles": 1000,
+            "max_groups": 1000,
+            "max_buckets": 1000,
+            "max_access_keys": 4
+        }
+
+        mock_set_quota.return_value = mock_return_value
+
+        controller = RgwUserAccountsController()
+        result = controller.set_quota(quota_type='account', account_id='RGW11111111111111111',
+                                      max_size='10GB', max_objects='1000')
+
+        mock_set_quota.assert_called_with('account', 'RGW11111111111111111', '10GB', '1000')
+
+        self.assertEqual(result, mock_return_value)
+
+    @patch.object(RgwAccounts, 'set_quota_status')
+    def test_set_quota_status(self, mock_set_quota_status):
+        mock_return_value = {
+            "id": "RGW11111111111111111",
+            "tenant": "",
+            "name": "Account1",
+            "email": "account1@ceph.com",
+            "quota": {
+                "enabled": True,
+                "check_on_raw": False,
+                "max_size": 10737418240,
+                "max_size_kb": 10485760,
+                "max_objects": 1000000
+            },
+            "bucket_quota": {
+                "enabled": False,
+                "check_on_raw": False,
+                "max_size": -1,
+                "max_size_kb": 0,
+                "max_objects": 1000000
+            },
+            "max_users": 1000,
+            "max_roles": 1000,
+            "max_groups": 1000,
+            "max_buckets": 1000,
+            "max_access_keys": 4
+        }
+
+        mock_set_quota_status.return_value = mock_return_value
+
+        controller = RgwUserAccountsController()
+        result = controller.set_quota_status(quota_type='account',
+                                             account_id='RGW11111111111111111',
+                                             quota_status='enabled')
+
+        mock_set_quota_status.assert_called_with('account', 'RGW11111111111111111', 'enabled')
+
+        self.assertEqual(result, mock_return_value)