]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Administration > Configuration > Some of the config options are not... 60798/head
authorNaman Munet <namanmunet@li-ff83bccc-26af-11b2-a85c-a4b04bfb1003.ibm.com>
Fri, 22 Nov 2024 09:57:44 +0000 (15:27 +0530)
committerNaman Munet <naman.munet@ibm.com>
Mon, 16 Dec 2024 16:21:20 +0000 (21:51 +0530)
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 <naman.munet@ibm.com>
qa/tasks/mgr/dashboard/test_rbd.py
src/pybind/mgr/dashboard/controllers/cluster_configuration.py
src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.po.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form-create-request.model.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.model.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html
src/pybind/mgr/dashboard/openapi.yaml

index a872645e33ed94388f9f18b343620ccdebf6fe5f..83b3bf520c288405012fe709e5772fc19d23c4b4 100644 (file)
@@ -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
index da5be2cc81d6a5a8a8ec3fa049efe97a49f6a101..292f381d79f0138a65e025b0d10ac61b3a5c5f88 100644 (file)
@@ -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)
 
index 983140a44c4bd159b4b295e90e4d0124d6caf6b5..b71719c4396c12b68f5aedbae9984e41164c157b 100644 (file)
@@ -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');
 
index 82e79a676ec60e052d0a11b95e6109d7a04bd04f..4132387d0f1a5683ac8643d300a878c996a3884b 100644 (file)
@@ -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();
+  }
 }
index 741c18d52a610e10ae0f6f9cb39871eb99719fe1..a6775dcee1750456d9237b4e4c1ca5406ca56b41 100644 (file)
       </div>
       <!-- Footer -->
       <div class="card-footer">
-        <cd-form-button-panel (submitActionEvent)="submit()"
+        <cd-form-button-panel (submitActionEvent)="forceUpdate ? openCriticalConfirmModal() : submit()"
                               [form]="configForm"
                               [submitText]="actionLabels.UPDATE"
                               wrappingClass="text-right"></cd-form-button-panel>
     </div>
   </form>
 </div>
+
index b6e9e700be464250c91d66dd7f6380ba0a46bdb8..118cb18430a3c4a52ef9653d1216a9239578cc7f 100644 (file)
@@ -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();
 
index a57603d4c8aa5b562cbac1cf003b0509e1a3b516..7a446ce808c8cad394f9957cbeae2af97cad5ae5 100644 (file)
@@ -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;
   }
 }
index d3ebc5f37c63dffad2ba02f973f4858360c30a56..0e1c0906f4ac1e4e632188d9174d6d06d9781283 100644 (file)
@@ -9,4 +9,5 @@ export class ConfigFormModel {
   min: any;
   max: any;
   services: Array<string>;
+  can_update_at_runtime: boolean;
 }
index 4b973187dbbae562b428e55c0f50f257f305d99c..90e3a1d2be8b20fdac20adfa0e4564b652728b24 100644 (file)
@@ -49,7 +49,7 @@
                         [form]="deletionForm"
                         [submitText]="(actionDescription | titlecase) + ' ' + itemDescription"
                         [modalForm]="true"
-                        [submitBtnType]="actionDescription === 'delete' || 'remove' ? 'danger' : 'primary'"></cd-form-button-panel>
+                        [submitBtnType]="(actionDescription === 'delete' || actionDescription === 'remove') ? 'danger' : 'primary'"></cd-form-button-panel>
 
 </cds-modal>
 
index b464344e27a2f76cc7a590529b25704ccd9d01ac..7d0ee83e6482ef56fe5d4b16603b1e4b53246f77 100644 (file)
@@ -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: