Merge pull request #60798 from rhcs-dashboard/edit-configuration-at-runtime

mgr/dashboard: Administration > Configuration > rgw_dns_name and rgw_dns_s3website_name not updatable via dashboard

Reviewed-by: Afreen Misbah <afreen@ibm.com>
This commit is contained in:
afreen23 2024-12-17 13:39:04 +05:30 committed by GitHub
commit 744eb5cdb3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 138 additions and 43 deletions

View 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

View 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)

View 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');

View 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();
}
}

View File

@ -1,4 +1,5 @@
export class ConfigFormCreateRequestModel {
name: string;
value: Array<any> = [];
force_update: boolean = false;
}

View File

@ -150,7 +150,7 @@
</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>
@ -158,3 +158,4 @@
</div>
</form>
</div>

View 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();

View 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;
}
}

View File

@ -9,4 +9,5 @@ export class ConfigFormModel {
min: any;
max: any;
services: Array<string>;
can_update_at_runtime: boolean;
}

View 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>

View 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: