Skip to content

Commit 3181acc

Browse files
Naman MunetNaman Munet
authored andcommitted
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 <[email protected]>
1 parent 862104b commit 3181acc

File tree

11 files changed

+138
-43
lines changed

11 files changed

+138
-43
lines changed

qa/tasks/mgr/dashboard/test_rbd.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,7 +869,19 @@ def _get_config_by_name(conf_name):
869869
self.assertEqual(clone_format_version, 2)
870870
self.assertStatus(200)
871871

872+
# if empty list is sent, then the config will remain as it is
872873
value = []
874+
res = [{'section': "global", 'value': "2"}]
875+
self._post('/api/cluster_conf', {
876+
'name': config_name,
877+
'value': value
878+
})
879+
self.wait_until_equal(
880+
lambda: _get_config_by_name(config_name),
881+
res,
882+
timeout=60)
883+
884+
value = [{'section': "global", 'value': ""}]
873885
self._post('/api/cluster_conf', {
874886
'name': config_name,
875887
'value': value

src/pybind/mgr/dashboard/controllers/cluster_configuration.py

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# -*- coding: utf-8 -*-
22

3+
from typing import Optional
4+
35
import cherrypy
46

57
from .. import mgr
68
from ..exceptions import DashboardException
79
from ..security import Scope
810
from ..services.ceph_service import CephService
9-
from . import APIDoc, APIRouter, EndpointDoc, RESTController
11+
from . import APIDoc, APIRouter, EndpointDoc, Param, RESTController
1012

1113
FILTER_SCHEMA = [{
1214
"name": (str, 'Name of the config option'),
@@ -80,22 +82,33 @@ def filter(self, names=None):
8082

8183
return config_options
8284

83-
def create(self, name, value):
85+
@EndpointDoc("Create/Update Cluster Configuration",
86+
parameters={
87+
'name': Param(str, 'Config option name'),
88+
'value': (
89+
[
90+
{
91+
'section': Param(
92+
str, 'Section/Client where config needs to be updated'
93+
),
94+
'value': Param(str, 'Value of the config option')
95+
}
96+
], 'Section and Value of the config option'
97+
),
98+
'force_update': Param(bool, 'Force update the config option', False, None)
99+
}
100+
)
101+
def create(self, name, value, force_update: Optional[bool] = None):
84102
# Check if config option is updateable at runtime
85-
self._updateable_at_runtime([name])
103+
self._updateable_at_runtime([name], force_update)
86104

87-
# Update config option
88-
avail_sections = ['global', 'mon', 'mgr', 'osd', 'mds', 'client']
105+
for entry in value:
106+
section = entry['section']
107+
entry_value = entry['value']
89108

90-
for section in avail_sections:
91-
for entry in value:
92-
if entry['value'] is None:
93-
break
94-
95-
if entry['section'] == section:
96-
CephService.send_command('mon', 'config set', who=section, name=name,
97-
value=str(entry['value']))
98-
break
109+
if entry_value not in (None, ''):
110+
CephService.send_command('mon', 'config set', who=section, name=name,
111+
value=str(entry_value))
99112
else:
100113
CephService.send_command('mon', 'config rm', who=section, name=name)
101114

@@ -116,11 +129,24 @@ def _get_config_option(self, name):
116129

117130
raise cherrypy.HTTPError(404)
118131

119-
def _updateable_at_runtime(self, config_option_names):
132+
def _updateable_at_runtime(self, config_option_names, force_update=False):
120133
not_updateable = []
121134

122135
for name in config_option_names:
123136
config_option = self._get_config_option(name)
137+
138+
# making rgw configuration to be editable by bypassing 'can_update_at_runtime'
139+
# as the same can be done via CLI.
140+
if force_update and 'rgw' in name and not config_option['can_update_at_runtime']:
141+
break
142+
143+
if force_update and 'rgw' not in name and not config_option['can_update_at_runtime']:
144+
raise DashboardException(
145+
msg=f'Only the configuration containing "rgw" can be edited at runtime with'
146+
f' force_update flag, hence not able to update "{name}"',
147+
code='config_option_not_updatable_at_runtime',
148+
component='cluster_configuration'
149+
)
124150
if not config_option['can_update_at_runtime']:
125151
not_updateable.append(name)
126152

src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.e2e-spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ describe('Configuration page', () => {
3030

3131
beforeEach(() => {
3232
configuration.clearTableSearchInput();
33-
configuration.getTableCount('found').as('configFound');
3433
});
3534

3635
after(() => {
@@ -50,6 +49,8 @@ describe('Configuration page', () => {
5049
});
5150

5251
it('should verify modified filter is applied properly', () => {
52+
configuration.clearFilter();
53+
configuration.getTableCount('found').as('configFound');
5354
configuration.filterTable('Modified', 'no');
5455
configuration.getTableCount('found').as('unmodifiedConfigs');
5556

src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/configuration.po.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ export class ConfigurationPageHelper extends PageHelper {
1212
configClear(name: string) {
1313
this.navigateTo();
1414
const valList = ['global', 'mon', 'mgr', 'osd', 'mds', 'client']; // Editable values
15-
1615
this.getFirstTableCell(name).click();
1716
cy.contains('button', 'Edit').click();
1817
// Waits for the data to load
@@ -26,6 +25,8 @@ export class ConfigurationPageHelper extends PageHelper {
2625

2726
cy.wait(3 * 1000);
2827

28+
this.clearFilter();
29+
2930
// Enter config setting name into filter box
3031
this.searchTable(name, 100);
3132

@@ -49,6 +50,7 @@ export class ConfigurationPageHelper extends PageHelper {
4950
* Ex: [global, '2'] is the global value with an input of 2
5051
*/
5152
edit(name: string, ...values: [string, string][]) {
53+
this.clearFilter();
5254
this.getFirstTableCell(name).click();
5355
cy.contains('button', 'Edit').click();
5456

@@ -78,4 +80,12 @@ export class ConfigurationPageHelper extends PageHelper {
7880
cy.contains('[data-testid=config-details-table]', `${value[0]}\: ${value[1]}`);
7981
});
8082
}
83+
84+
clearFilter() {
85+
cy.get('div.filter-tags') // Find the div with class filter-tags
86+
.find('button.cds--btn.cds--btn--ghost') // Find the button with specific classes
87+
.contains('Clear filters') // Ensure the button contains the text "Clear filters"
88+
.should('be.visible') // Assert that the button is visible
89+
.click();
90+
}
8191
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export class ConfigFormCreateRequestModel {
22
name: string;
33
value: Array<any> = [];
4+
force_update: boolean = false;
45
}

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,12 @@
150150
</div>
151151
<!-- Footer -->
152152
<div class="card-footer">
153-
<cd-form-button-panel (submitActionEvent)="submit()"
153+
<cd-form-button-panel (submitActionEvent)="forceUpdate ? openCriticalConfirmModal() : submit()"
154154
[form]="configForm"
155155
[submitText]="actionLabels.UPDATE"
156156
wrappingClass="text-right"></cd-form-button-panel>
157157
</div>
158158
</div>
159159
</form>
160160
</div>
161+

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import { CdForm } from '~/app/shared/forms/cd-form';
1313
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
1414
import { NotificationService } from '~/app/shared/services/notification.service';
1515
import { ConfigFormCreateRequestModel } from './configuration-form-create-request.model';
16+
import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
17+
import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
18+
19+
const RGW = 'rgw';
1620

1721
@Component({
1822
selector: 'cd-configuration-form',
@@ -29,13 +33,15 @@ export class ConfigurationFormComponent extends CdForm implements OnInit {
2933
maxValue: number;
3034
patternHelpText: string;
3135
availSections = ['global', 'mon', 'mgr', 'osd', 'mds', 'client'];
36+
forceUpdate: boolean;
3237

3338
constructor(
3439
public actionLabels: ActionLabelsI18n,
3540
private route: ActivatedRoute,
3641
private router: Router,
3742
private configService: ConfigurationService,
38-
private notificationService: NotificationService
43+
private notificationService: NotificationService,
44+
private modalService: ModalCdsService
3945
) {
4046
super();
4147
this.createForm();
@@ -95,7 +101,6 @@ export class ConfigurationFormComponent extends CdForm implements OnInit {
95101
setResponse(response: ConfigFormModel) {
96102
this.response = response;
97103
const validators = this.getValidators(response);
98-
99104
this.configForm.get('name').setValue(response.name);
100105
this.configForm.get('desc').setValue(response.desc);
101106
this.configForm.get('long_desc').setValue(response.long_desc);
@@ -118,7 +123,7 @@ export class ConfigurationFormComponent extends CdForm implements OnInit {
118123
this.configForm.get('values').get(value.section).setValue(sectionValue);
119124
});
120125
}
121-
126+
this.forceUpdate = !this.response.can_update_at_runtime && response.name.includes(RGW);
122127
this.availSections.forEach((section) => {
123128
this.configForm.get('values').get(section).setValidators(validators);
124129
});
@@ -134,7 +139,7 @@ export class ConfigurationFormComponent extends CdForm implements OnInit {
134139

135140
this.availSections.forEach((section) => {
136141
const sectionValue = this.configForm.getValue(section);
137-
if (sectionValue !== null && sectionValue !== '') {
142+
if (sectionValue !== null) {
138143
values.push({ section: section, value: sectionValue });
139144
}
140145
});
@@ -143,12 +148,28 @@ export class ConfigurationFormComponent extends CdForm implements OnInit {
143148
const request = new ConfigFormCreateRequestModel();
144149
request.name = this.configForm.getValue('name');
145150
request.value = values;
151+
if (this.forceUpdate) {
152+
request.force_update = this.forceUpdate;
153+
}
146154
return request;
147155
}
148156

149157
return null;
150158
}
151159

160+
openCriticalConfirmModal() {
161+
this.modalService.show(CriticalConfirmationModalComponent, {
162+
buttonText: $localize`Force Edit`,
163+
actionDescription: $localize`force edit`,
164+
itemDescription: $localize`configuration`,
165+
infoMessage: 'Updating this configuration might require restarting the client',
166+
submitAction: () => {
167+
this.modalService.dismissAll();
168+
this.submit();
169+
}
170+
});
171+
}
172+
152173
submit() {
153174
const request = this.createRequest();
154175

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
1212
import { Permission } from '~/app/shared/models/permissions';
1313
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
1414

15+
const RGW = 'rgw';
16+
1517
@Component({
1618
selector: 'cd-configuration',
1719
templateUrl: './configuration.component.html',
@@ -25,11 +27,27 @@ export class ConfigurationComponent extends ListWithDetails implements OnInit {
2527
columns: CdTableColumn[];
2628
selection = new CdTableSelection();
2729
filters: CdTableColumn[] = [
30+
{
31+
name: $localize`Modified`,
32+
prop: 'modified',
33+
filterOptions: [$localize`yes`, $localize`no`],
34+
filterInitValue: $localize`yes`,
35+
filterPredicate: (row, value) => {
36+
if (value === 'yes' && row.hasOwnProperty('value')) {
37+
return true;
38+
}
39+
40+
if (value === 'no' && !row.hasOwnProperty('value')) {
41+
return true;
42+
}
43+
44+
return false;
45+
}
46+
},
2847
{
2948
name: $localize`Level`,
3049
prop: 'level',
3150
filterOptions: ['basic', 'advanced', 'dev'],
32-
filterInitValue: 'basic',
3351
filterPredicate: (row, value) => {
3452
enum Level {
3553
basic = 0,
@@ -60,22 +78,6 @@ export class ConfigurationComponent extends ListWithDetails implements OnInit {
6078
}
6179
return row.source.includes(value);
6280
}
63-
},
64-
{
65-
name: $localize`Modified`,
66-
prop: 'modified',
67-
filterOptions: ['yes', 'no'],
68-
filterPredicate: (row, value) => {
69-
if (value === 'yes' && row.hasOwnProperty('value')) {
70-
return true;
71-
}
72-
73-
if (value === 'no' && !row.hasOwnProperty('value')) {
74-
return true;
75-
}
76-
77-
return false;
78-
}
7981
}
8082
];
8183

@@ -143,7 +145,9 @@ export class ConfigurationComponent extends ListWithDetails implements OnInit {
143145
if (selection.selected.length !== 1) {
144146
return false;
145147
}
146-
147-
return selection.selected[0].can_update_at_runtime;
148+
if ((this.selection.selected[0].name as string).includes(RGW)) {
149+
return true;
150+
}
151+
return this.selection.selected[0].can_update_at_runtime;
148152
}
149153
}

src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ export class ConfigFormModel {
99
min: any;
1010
max: any;
1111
services: Array<string>;
12+
can_update_at_runtime: boolean;
1213
}

src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
[form]="deletionForm"
5050
[submitText]="(actionDescription | titlecase) + ' ' + itemDescription"
5151
[modalForm]="true"
52-
[submitBtnType]="actionDescription === 'delete' || 'remove' ? 'danger' : 'primary'"></cd-form-button-panel>
52+
[submitBtnType]="(actionDescription === 'delete' || actionDescription === 'remove') ? 'danger' : 'primary'"></cd-form-button-panel>
5353

5454
</cds-modal>
5555

0 commit comments

Comments
 (0)