diff options
Diffstat (limited to 'src/pybind/mgr/dashboard/controllers')
-rw-r--r-- | src/pybind/mgr/dashboard/controllers/cephfs.py | 3 | ||||
-rw-r--r-- | src/pybind/mgr/dashboard/controllers/cluster_configuration.py | 56 | ||||
-rwxr-xr-x | src/pybind/mgr/dashboard/controllers/rgw.py | 13 | ||||
-rw-r--r-- | src/pybind/mgr/dashboard/controllers/rgw_iam.py | 52 | ||||
-rw-r--r-- | src/pybind/mgr/dashboard/controllers/smb.py | 186 |
5 files changed, 286 insertions, 24 deletions
diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py index 9f9b7501f44..d05b7551365 100644 --- a/src/pybind/mgr/dashboard/controllers/cephfs.py +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -2,7 +2,6 @@ # pylint: disable=too-many-lines import errno import json -import logging import os from collections import defaultdict from typing import Any, Dict, List @@ -30,8 +29,6 @@ GET_STATFS_SCHEMA = { 'subdirs': (int, '') } -logger = logging.getLogger("controllers.rgw") - # pylint: disable=R0904 @APIRouter('/cephfs', Scope.CEPHFS) diff --git a/src/pybind/mgr/dashboard/controllers/cluster_configuration.py b/src/pybind/mgr/dashboard/controllers/cluster_configuration.py index da5be2cc81d..292f381d79f 100644 --- a/src/pybind/mgr/dashboard/controllers/cluster_configuration.py +++ b/src/pybind/mgr/dashboard/controllers/cluster_configuration.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- +from typing import Optional + import cherrypy from .. import mgr from ..exceptions import DashboardException from ..security import Scope from ..services.ceph_service import CephService -from . import APIDoc, APIRouter, EndpointDoc, RESTController +from . import APIDoc, APIRouter, EndpointDoc, Param, RESTController FILTER_SCHEMA = [{ "name": (str, 'Name of the config option'), @@ -80,22 +82,33 @@ class ClusterConfiguration(RESTController): return config_options - def create(self, name, value): + @EndpointDoc("Create/Update Cluster Configuration", + parameters={ + 'name': Param(str, 'Config option name'), + 'value': ( + [ + { + 'section': Param( + str, 'Section/Client where config needs to be updated' + ), + 'value': Param(str, 'Value of the config option') + } + ], 'Section and Value of the config option' + ), + 'force_update': Param(bool, 'Force update the config option', False, None) + } + ) + def create(self, name, value, force_update: Optional[bool] = None): # Check if config option is updateable at runtime - self._updateable_at_runtime([name]) + self._updateable_at_runtime([name], force_update) - # Update config option - avail_sections = ['global', 'mon', 'mgr', 'osd', 'mds', 'client'] + for entry in value: + section = entry['section'] + entry_value = entry['value'] - for section in avail_sections: - for entry in value: - if entry['value'] is None: - break - - if entry['section'] == section: - CephService.send_command('mon', 'config set', who=section, name=name, - value=str(entry['value'])) - break + if entry_value not in (None, ''): + CephService.send_command('mon', 'config set', who=section, name=name, + value=str(entry_value)) else: CephService.send_command('mon', 'config rm', who=section, name=name) @@ -116,11 +129,24 @@ class ClusterConfiguration(RESTController): raise cherrypy.HTTPError(404) - def _updateable_at_runtime(self, config_option_names): + def _updateable_at_runtime(self, config_option_names, force_update=False): not_updateable = [] for name in config_option_names: config_option = self._get_config_option(name) + + # making rgw configuration to be editable by bypassing 'can_update_at_runtime' + # as the same can be done via CLI. + if force_update and 'rgw' in name and not config_option['can_update_at_runtime']: + break + + if force_update and 'rgw' not in name and not config_option['can_update_at_runtime']: + raise DashboardException( + msg=f'Only the configuration containing "rgw" can be edited at runtime with' + f' force_update flag, hence not able to update "{name}"', + code='config_option_not_updatable_at_runtime', + component='cluster_configuration' + ) if not config_option['can_update_at_runtime']: not_updateable.append(name) diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index 9d257674794..d48542a7590 100755 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -106,13 +106,11 @@ class RgwMultisiteStatus(RESTController): @allow_empty_body # pylint: disable=W0102,W0613 def migrate(self, daemon_name=None, realm_name=None, zonegroup_name=None, zone_name=None, - zonegroup_endpoints=None, zone_endpoints=None, access_key=None, - secret_key=None): + zonegroup_endpoints=None, zone_endpoints=None, username=None): multisite_instance = RgwMultisite() result = multisite_instance.migrate_to_multisite(realm_name, zonegroup_name, zone_name, zonegroup_endpoints, - zone_endpoints, access_key, - secret_key) + zone_endpoints, username) return result @RESTController.Collection(method='POST', path='/multisite-replications') @@ -773,6 +771,9 @@ class RgwUser(RgwRESTController): return users def get(self, uid, daemon_name=None, stats=True) -> dict: + return self._get(uid, daemon_name=daemon_name, stats=stats) + + def _get(self, uid, daemon_name=None, stats=True) -> dict: query_params = '?stats' if stats else '' result = self.proxy(daemon_name, 'GET', 'user{}'.format(query_params), {'uid': uid, 'stats': stats}) @@ -788,7 +789,7 @@ class RgwUser(RgwRESTController): # type: (Optional[str]) -> List[str] emails = [] for uid in json.loads(self.list(daemon_name)): # type: ignore - user = json.loads(self.get(uid, daemon_name)) # type: ignore + user = self._get(uid, daemon_name) # type: ignore if user["email"]: emails.append(user["email"]) return emails @@ -910,7 +911,7 @@ class RgwUser(RgwRESTController): secret_key=None, daemon_name=None): # pylint: disable=R1705 subusr_array = [] - user = json.loads(self.get(uid, daemon_name)) # type: ignore + user = self._get(uid, daemon_name) # type: ignore subusers = user["subusers"] for sub_usr in subusers: subusr_array.append(sub_usr["id"]) diff --git a/src/pybind/mgr/dashboard/controllers/rgw_iam.py b/src/pybind/mgr/dashboard/controllers/rgw_iam.py new file mode 100644 index 00000000000..458bbbb7321 --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/rgw_iam.py @@ -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) diff --git a/src/pybind/mgr/dashboard/controllers/smb.py b/src/pybind/mgr/dashboard/controllers/smb.py new file mode 100644 index 00000000000..97eff8c3dfe --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/smb.py @@ -0,0 +1,186 @@ + +# -*- coding: utf-8 -*- + +import json +import logging +from typing import List + +from smb.enums import Intent +from smb.proto import Simplified +from smb.resources import Cluster, Share + +from dashboard.controllers._docs import EndpointDoc +from dashboard.controllers._permissions import CreatePermission, DeletePermission +from dashboard.exceptions import DashboardException + +from .. import mgr +from ..security import Scope +from . import APIDoc, APIRouter, ReadPermission, RESTController + +logger = logging.getLogger('controllers.smb') + +CLUSTER_SCHEMA = { + "resource_type": (str, "ceph.smb.cluster"), + "cluster_id": (str, "Unique identifier for the cluster"), + "auth_mode": (str, "Either 'active-directory' or 'user'"), + "intent": (str, "Desired state of the resource, e.g., 'present' or 'removed'"), + "domain_settings": ({ + "realm": (str, "Domain realm, e.g., 'DOMAIN1.SINK.TEST'"), + "join_sources": ([{ + "source_type": (str, "resource"), + "ref": (str, "Reference identifier for the join auth resource") + }], "List of join auth sources for domain settings") + }, "Domain-specific settings for active-directory auth mode"), + "user_group_settings": ([{ + "source_type": (str, "resource"), + "ref": (str, "Reference identifier for the user group resource") + }], "User group settings for user auth mode"), + "custom_dns": ([str], "List of custom DNS server addresses"), + "placement": ({ + "count": (int, "Number of instances to place") + }, "Placement configuration for the resource") +} + +CLUSTER_SCHEMA_RESULTS = { + "results": ([{ + "resource": ({ + "resource_type": (str, "ceph.smb.cluster"), + "cluster_id": (str, "Unique identifier for the cluster"), + "auth_mode": (str, "Either 'active-directory' or 'user'"), + "intent": (str, "Desired state of the resource, e.g., 'present' or 'removed'"), + "domain_settings": ({ + "realm": (str, "Domain realm, e.g., 'DOMAIN1.SINK.TEST'"), + "join_sources": ([{ + "source_type": (str, "resource"), + "ref": (str, "Reference identifier for the join auth resource") + }], "List of join auth sources for domain settings") + }, "Domain-specific settings for active-directory auth mode"), + "user_group_settings": ([{ + "source_type": (str, "resource"), + "ref": (str, "Reference identifier for the user group resource") + }], "User group settings for user auth mode (optional)"), + "custom_dns": ([str], "List of custom DNS server addresses (optional)"), + "placement": ({ + "count": (int, "Number of instances to place") + }, "Placement configuration for the resource (optional)"), + }, "Resource details"), + "state": (str, "State of the resource"), + "success": (bool, "Indicates whether the operation was successful") + }], "List of results with resource details"), + "success": (bool, "Overall success status of the operation") +} + +LIST_CLUSTER_SCHEMA = [CLUSTER_SCHEMA] + +SHARE_SCHEMA = { + "resource_type": (str, "ceph.smb.share"), + "cluster_id": (str, "Unique identifier for the cluster"), + "share_id": (str, "Unique identifier for the share"), + "intent": (str, "Desired state of the resource, e.g., 'present' or 'removed'"), + "name": (str, "Name of the share"), + "readonly": (bool, "Indicates if the share is read-only"), + "browseable": (bool, "Indicates if the share is browseable"), + "cephfs": ({ + "volume": (str, "Name of the CephFS file system"), + "path": (str, "Path within the CephFS file system"), + "provider": (str, "Provider of the CephFS share, e.g., 'samba-vfs'") + }, "Configuration for the CephFS share") +} + + +@APIRouter('/smb/cluster', Scope.SMB) +@APIDoc("SMB Cluster Management API", "SMB") +class SMBCluster(RESTController): + _resource: str = 'ceph.smb.cluster' + + @ReadPermission + @EndpointDoc("List smb clusters", + responses={200: LIST_CLUSTER_SCHEMA}) + def list(self) -> List[Cluster]: + """ + List smb clusters + """ + res = mgr.remote('smb', 'show', [self._resource]) + return res['resources'] if 'resources' in res else [res] + + @ReadPermission + @EndpointDoc("Get an smb cluster", + parameters={ + 'cluster_id': (str, 'Unique identifier for the cluster') + }, + responses={200: CLUSTER_SCHEMA}) + def get(self, cluster_id: str) -> Cluster: + """ + Get an smb cluster by cluster id + """ + return mgr.remote('smb', 'show', [f'{self._resource}.{cluster_id}']) + + @CreatePermission + @EndpointDoc("Create smb cluster", + parameters={ + 'cluster_resource': (str, 'cluster_resource') + }, + responses={201: CLUSTER_SCHEMA_RESULTS}) + def create(self, cluster_resource: Cluster) -> Simplified: + """ + Create an smb cluster + + :param cluster_resource: Dict cluster data + :return: Returns cluster resource. + :rtype: Dict[str, Any] + """ + try: + return mgr.remote( + 'smb', + 'apply_resources', + json.dumps(cluster_resource)).to_simplified() + except RuntimeError as e: + raise DashboardException(e, component='smb') + + +@APIRouter('/smb/share', Scope.SMB) +@APIDoc("SMB Share Management API", "SMB") +class SMBShare(RESTController): + _resource: str = 'ceph.smb.share' + + @ReadPermission + @EndpointDoc("List smb shares", + parameters={ + 'cluster_id': (str, 'Unique identifier for the cluster') + }, + responses={200: SHARE_SCHEMA}) + def list(self, cluster_id: str = '') -> List[Share]: + """ + List all smb shares or all shares for a given cluster + + :param cluster_id: Dict containing cluster information + :return: Returns list of shares. + :rtype: List[Dict] + """ + res = mgr.remote( + 'smb', + 'show', + [f'{self._resource}.{cluster_id}' if cluster_id else self._resource]) + return res['resources'] if 'resources' in res else res + + @DeletePermission + @EndpointDoc("Remove smb shares", + parameters={ + 'cluster_id': (str, 'Unique identifier for the cluster'), + 'share_id': (str, 'Unique identifier for the share') + }, + responses={204: None}) + def delete(self, cluster_id: str, share_id: str): + """ + Remove an smb share from a given cluster + + :param cluster_id: Cluster identifier + :param share_id: Share identifier + :return: None. + """ + resource = {} + resource['resource_type'] = self._resource + resource['cluster_id'] = cluster_id + resource['share_id'] = share_id + resource['intent'] = Intent.REMOVED + return mgr.remote('smb', 'apply_resources', json.dumps(resource)).one().to_simplified() |