From: Naman Munet Date: Fri, 22 Nov 2024 09:57:44 +0000 (+0530) Subject: mgr/dashboard: Administration > Configuration > Some of the config options are not... X-Git-Tag: v20.0.0~517^2 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=refs%2Fpull%2F60798%2Fhead;p=ceph.git mgr/dashboard: Administration > Configuration > Some of the config options are not updatable at runtime Fixes: https://tracker.ceph.com/issues/68976 Fixes Includes: 1) by-passing 'can_update_at_runtime' flag for 'rgw' related configurations as the same can be updated at runtime via CLI. Also implemented a warning popup for user to make force edit to rgw related configurations. 2) when navigated to Administration >> Configuration, modified configuration will be seen as we see in cli "ceph config dump", instead of configuration with filter level:basic Signed-off-by: Naman Munet --- diff --git a/qa/tasks/mgr/dashboard/test_rbd.py b/qa/tasks/mgr/dashboard/test_rbd.py index a872645e33ed9..83b3bf520c288 100644 --- a/qa/tasks/mgr/dashboard/test_rbd.py +++ b/qa/tasks/mgr/dashboard/test_rbd.py @@ -869,7 +869,19 @@ class RbdTest(DashboardTestCase): self.assertEqual(clone_format_version, 2) self.assertStatus(200) + # if empty list is sent, then the config will remain as it is value = [] + res = [{'section': "global", 'value': "2"}] + self._post('/api/cluster_conf', { + 'name': config_name, + 'value': value + }) + self.wait_until_equal( + lambda: _get_config_by_name(config_name), + res, + timeout=60) + + value = [{'section': "global", 'value': ""}] self._post('/api/cluster_conf', { 'name': config_name, 'value': value diff --git a/src/pybind/mgr/dashboard/controllers/cluster_configuration.py b/src/pybind/mgr/dashboard/controllers/cluster_configuration.py index da5be2cc81d6a..292f381d79f01 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/frontend/cypress/e2e/cluster/configuration.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.e2e-spec.ts index 983140a44c4bd..b71719c4396c1 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.e2e-spec.ts @@ -30,7 +30,6 @@ describe('Configuration page', () => { beforeEach(() => { configuration.clearTableSearchInput(); - configuration.getTableCount('found').as('configFound'); }); after(() => { @@ -50,6 +49,8 @@ describe('Configuration page', () => { }); it('should verify modified filter is applied properly', () => { + configuration.clearFilter(); + configuration.getTableCount('found').as('configFound'); configuration.filterTable('Modified', 'no'); configuration.getTableCount('found').as('unmodifiedConfigs'); diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.po.ts index 82e79a676ec60..4132387d0f1a5 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.po.ts @@ -12,7 +12,6 @@ export class ConfigurationPageHelper extends PageHelper { configClear(name: string) { this.navigateTo(); const valList = ['global', 'mon', 'mgr', 'osd', 'mds', 'client']; // Editable values - this.getFirstTableCell(name).click(); cy.contains('button', 'Edit').click(); // Waits for the data to load @@ -26,6 +25,8 @@ export class ConfigurationPageHelper extends PageHelper { cy.wait(3 * 1000); + this.clearFilter(); + // Enter config setting name into filter box this.searchTable(name, 100); @@ -49,6 +50,7 @@ export class ConfigurationPageHelper extends PageHelper { * Ex: [global, '2'] is the global value with an input of 2 */ edit(name: string, ...values: [string, string][]) { + this.clearFilter(); this.getFirstTableCell(name).click(); cy.contains('button', 'Edit').click(); @@ -78,4 +80,12 @@ export class ConfigurationPageHelper extends PageHelper { cy.contains('[data-testid=config-details-table]', `${value[0]}\: ${value[1]}`); }); } + + clearFilter() { + cy.get('div.filter-tags') // Find the div with class filter-tags + .find('button.cds--btn.cds--btn--ghost') // Find the button with specific classes + .contains('Clear filters') // Ensure the button contains the text "Clear filters" + .should('be.visible') // Assert that the button is visible + .click(); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form-create-request.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form-create-request.model.ts index bca65a887c5d0..b6d23e35c9d02 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form-create-request.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form-create-request.model.ts @@ -1,4 +1,5 @@ export class ConfigFormCreateRequestModel { name: string; value: Array = []; + force_update: boolean = false; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.html index 741c18d52a610..a6775dcee1750 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.html @@ -150,7 +150,7 @@ + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.ts index b6e9e700be464..118cb18430a3c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.ts @@ -13,6 +13,10 @@ import { CdForm } from '~/app/shared/forms/cd-form'; import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; import { NotificationService } from '~/app/shared/services/notification.service'; import { ConfigFormCreateRequestModel } from './configuration-form-create-request.model'; +import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; +import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; + +const RGW = 'rgw'; @Component({ selector: 'cd-configuration-form', @@ -29,13 +33,15 @@ export class ConfigurationFormComponent extends CdForm implements OnInit { maxValue: number; patternHelpText: string; availSections = ['global', 'mon', 'mgr', 'osd', 'mds', 'client']; + forceUpdate: boolean; constructor( public actionLabels: ActionLabelsI18n, private route: ActivatedRoute, private router: Router, private configService: ConfigurationService, - private notificationService: NotificationService + private notificationService: NotificationService, + private modalService: ModalCdsService ) { super(); this.createForm(); @@ -95,7 +101,6 @@ export class ConfigurationFormComponent extends CdForm implements OnInit { setResponse(response: ConfigFormModel) { this.response = response; const validators = this.getValidators(response); - this.configForm.get('name').setValue(response.name); this.configForm.get('desc').setValue(response.desc); this.configForm.get('long_desc').setValue(response.long_desc); @@ -118,7 +123,7 @@ export class ConfigurationFormComponent extends CdForm implements OnInit { this.configForm.get('values').get(value.section).setValue(sectionValue); }); } - + this.forceUpdate = !this.response.can_update_at_runtime && response.name.includes(RGW); this.availSections.forEach((section) => { this.configForm.get('values').get(section).setValidators(validators); }); @@ -134,7 +139,7 @@ export class ConfigurationFormComponent extends CdForm implements OnInit { this.availSections.forEach((section) => { const sectionValue = this.configForm.getValue(section); - if (sectionValue !== null && sectionValue !== '') { + if (sectionValue !== null) { values.push({ section: section, value: sectionValue }); } }); @@ -143,12 +148,28 @@ export class ConfigurationFormComponent extends CdForm implements OnInit { const request = new ConfigFormCreateRequestModel(); request.name = this.configForm.getValue('name'); request.value = values; + if (this.forceUpdate) { + request.force_update = this.forceUpdate; + } return request; } return null; } + openCriticalConfirmModal() { + this.modalService.show(CriticalConfirmationModalComponent, { + buttonText: $localize`Force Edit`, + actionDescription: $localize`force edit`, + itemDescription: $localize`configuration`, + infoMessage: 'Updating this configuration might require restarting the client', + submitAction: () => { + this.modalService.dismissAll(); + this.submit(); + } + }); + } + submit() { const request = this.createRequest(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.ts index a57603d4c8aa5..7a446ce808c8c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.ts @@ -12,6 +12,8 @@ import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; import { Permission } from '~/app/shared/models/permissions'; import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +const RGW = 'rgw'; + @Component({ selector: 'cd-configuration', templateUrl: './configuration.component.html', @@ -25,11 +27,27 @@ export class ConfigurationComponent extends ListWithDetails implements OnInit { columns: CdTableColumn[]; selection = new CdTableSelection(); filters: CdTableColumn[] = [ + { + name: $localize`Modified`, + prop: 'modified', + filterOptions: [$localize`yes`, $localize`no`], + filterInitValue: $localize`yes`, + filterPredicate: (row, value) => { + if (value === 'yes' && row.hasOwnProperty('value')) { + return true; + } + + if (value === 'no' && !row.hasOwnProperty('value')) { + return true; + } + + return false; + } + }, { name: $localize`Level`, prop: 'level', filterOptions: ['basic', 'advanced', 'dev'], - filterInitValue: 'basic', filterPredicate: (row, value) => { enum Level { basic = 0, @@ -60,22 +78,6 @@ export class ConfigurationComponent extends ListWithDetails implements OnInit { } return row.source.includes(value); } - }, - { - name: $localize`Modified`, - prop: 'modified', - filterOptions: ['yes', 'no'], - filterPredicate: (row, value) => { - if (value === 'yes' && row.hasOwnProperty('value')) { - return true; - } - - if (value === 'no' && !row.hasOwnProperty('value')) { - return true; - } - - return false; - } } ]; @@ -143,7 +145,9 @@ export class ConfigurationComponent extends ListWithDetails implements OnInit { if (selection.selected.length !== 1) { return false; } - - return selection.selected[0].can_update_at_runtime; + if ((this.selection.selected[0].name as string).includes(RGW)) { + return true; + } + return this.selection.selected[0].can_update_at_runtime; } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.model.ts index d3ebc5f37c63d..0e1c0906f4ac1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.model.ts @@ -9,4 +9,5 @@ export class ConfigFormModel { min: any; max: any; services: Array; + can_update_at_runtime: boolean; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html index 4b973187dbbae..90e3a1d2be8b2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html @@ -49,7 +49,7 @@ [form]="deletionForm" [submitText]="(actionDescription | titlecase) + ' ' + itemDescription" [modalForm]="true" - [submitBtnType]="actionDescription === 'delete' || 'remove' ? 'danger' : 'primary'"> + [submitBtnType]="(actionDescription === 'delete' || actionDescription === 'remove') ? 'danger' : 'primary'"> diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index b464344e27a2f..7d0ee83e6482e 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -3977,10 +3977,27 @@ paths: application/json: schema: properties: + force_update: + description: Force update the config option + type: boolean name: + description: Config option name type: string value: - type: string + description: Section and Value of the config option + items: + properties: + section: + description: Section/Client where config needs to be updated + type: string + value: + description: Value of the config option + type: string + required: + - section + - value + type: object + type: array required: - name - value @@ -4007,6 +4024,7 @@ paths: trace. security: - jwt: [] + summary: Create/Update Cluster Configuration tags: - ClusterConfiguration put: