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
# -*- 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'),
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)
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)
beforeEach(() => {
configuration.clearTableSearchInput();
- configuration.getTableCount('found').as('configFound');
});
after(() => {
});
it('should verify modified filter is applied properly', () => {
+ configuration.clearFilter();
+ configuration.getTableCount('found').as('configFound');
configuration.filterTable('Modified', 'no');
configuration.getTableCount('found').as('unmodifiedConfigs');
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
cy.wait(3 * 1000);
+ this.clearFilter();
+
// Enter config setting name into filter box
this.searchTable(name, 100);
* 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();
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();
+ }
}
export class ConfigFormCreateRequestModel {
name: string;
value: Array<any> = [];
+ force_update: boolean = false;
}
</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>
+
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',
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();
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);
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);
});
this.availSections.forEach((section) => {
const sectionValue = this.configForm.getValue(section);
- if (sectionValue !== null && sectionValue !== '') {
+ if (sectionValue !== null) {
values.push({ section: section, value: sectionValue });
}
});
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();
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',
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,
}
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;
- }
}
];
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;
}
}
min: any;
max: any;
services: Array<string>;
+ can_update_at_runtime: boolean;
}
[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>
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
trace.
security:
- jwt: []
+ summary: Create/Update Cluster Configuration
tags:
- ClusterConfiguration
put: