]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add config filter and delete routes
authorTatjana Dehler <tdehler@suse.com>
Wed, 13 Mar 2019 13:51:43 +0000 (14:51 +0100)
committerTatjana Dehler <tdehler@suse.com>
Tue, 28 May 2019 14:14:33 +0000 (16:14 +0200)
Add a filter route to the configurations endpoint to get a subset of
config options in one request.
Add a delete route to the configurations endpoint to delete a
specific config option value.

The commit contains the frontend and backend related changes.

It also adds the missing '/' to `ConfigurationService.bulkCreate` and
unit test.

Signed-off-by: Tatjana Dehler <tdehler@suse.com>
qa/tasks/mgr/dashboard/test_cluster_configuration.py
src/pybind/mgr/dashboard/controllers/cluster_configuration.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/configuration.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/configuration.service.ts

index 2a718447c1c6160da0d348480d5bee7125daa44a..4112273e6400ae61b1b28a7419891b3a7a61f0ff 100644 (file)
@@ -43,6 +43,36 @@ class ClusterConfigurationTest(DashboardTestCase):
         if orig_value:
             self._ceph_cmd(['config', 'set', 'mon', config_name, orig_value[0]['value']])
 
+    def test_filter_config_options(self):
+        config_names = ['osd_scrub_during_recovery', 'osd_scrub_begin_hour', 'osd_scrub_end_hour']
+        data = self._get('/api/cluster_conf/filter?names={}'.format(','.join(config_names)))
+        self.assertStatus(200)
+        self.assertIsInstance(data, list)
+        self.assertEqual(len(data), 3)
+        for conf in data:
+            self._validate_single(conf)
+            self.assertIn(conf['name'], config_names)
+
+    def test_filter_config_options_empty_names(self):
+        self._get('/api/cluster_conf/filter?names=')
+        self.assertStatus(404)
+        self.assertEqual(self._resp.json()['detail'], 'Config options `` not found')
+
+    def test_filter_config_options_unknown_name(self):
+        self._get('/api/cluster_conf/filter?names=abc')
+        self.assertStatus(404)
+        self.assertEqual(self._resp.json()['detail'], 'Config options `abc` not found')
+
+    def test_filter_config_options_contains_unknown_name(self):
+        config_names = ['osd_scrub_during_recovery', 'osd_scrub_begin_hour', 'abc']
+        data = self._get('/api/cluster_conf/filter?names={}'.format(','.join(config_names)))
+        self.assertStatus(200)
+        self.assertIsInstance(data, list)
+        self.assertEqual(len(data), 2)
+        for conf in data:
+            self._validate_single(conf)
+            self.assertIn(conf['name'], config_names)
+
     def test_create(self):
         config_name = 'debug_ms'
         orig_value = self._get_config_by_name(config_name)
@@ -65,6 +95,29 @@ class ClusterConfigurationTest(DashboardTestCase):
         self._clear_all_values_for_config_option(config_name)
         self._reset_original_values(config_name, orig_value)
 
+    def test_delete(self):
+        config_name = 'debug_ms'
+        orig_value = self._get_config_by_name(config_name)
+
+        # set a config option
+        expected_result = [{'section': 'mon', 'value': '0/3'}]
+        self._post('/api/cluster_conf', {
+            'name': config_name,
+            'value': expected_result
+        })
+        self.assertStatus(201)
+        self._wait_for_expected_get_result(self._get_config_by_name, config_name, expected_result)
+
+        # delete it and check if it's deleted
+        self._delete('/api/cluster_conf/{}?section={}'.format(config_name, 'mon'))
+        self.assertStatus(204)
+        result = self._wait_for_expected_get_result(self._get_config_by_name, config_name, None)
+        self.assertEqual(result, None)
+
+        # reset original value
+        self._clear_all_values_for_config_option(config_name)
+        self._reset_original_values(config_name, orig_value)
+
     def test_create_cant_update_at_runtime(self):
         config_name = 'public_bind_addr'  # not updatable
         config_value = [{'section': 'global', 'value': 'true'}]
index fcf3f81914540fe18102c27c2f33ccb5b99fe93e..dc3d1ab904056203fa3f456db44d7cf913469cd5 100644 (file)
@@ -38,6 +38,22 @@ class ClusterConfiguration(RESTController):
     def get(self, name):
         return self._get_config_option(name)
 
+    @RESTController.Collection('GET', query_params=['name'])
+    def filter(self, names=None):
+        config_options = []
+
+        if names:
+            for name in names.split(','):
+                try:
+                    config_options.append(self._get_config_option(name))
+                except cherrypy.HTTPError:
+                    pass
+
+        if not config_options:
+            raise cherrypy.HTTPError(404, 'Config options `{}` not found'.format(names))
+
+        return config_options
+
     def create(self, name, value):
         # Check if config option is updateable at runtime
         self._updateable_at_runtime([name])
@@ -57,6 +73,9 @@ class ClusterConfiguration(RESTController):
             else:
                 CephService.send_command('mon', 'config rm', who=section, name=name)
 
+    def delete(self, name, section):
+        return CephService.send_command('mon', 'config rm', who=section, name=name)
+
     def bulk_set(self, options):
         self._updateable_at_runtime(options.keys())
 
index ee3ba222979af7c91a713a0e9b489bb05b9d2abc..b373629132751c833281cfff7b5523185ee8e2f2 100755 (executable)
@@ -6,14 +6,17 @@ import { RouterTestingModule } from '@angular/router/testing';
 import * as _ from 'lodash';
 import { ToastModule } from 'ng2-toastr';
 import { BsModalRef, ModalModule } from 'ngx-bootstrap/modal';
+import { of as observableOf } from 'rxjs';
 
 import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper';
+import { ConfigurationService } from '../../../../shared/api/configuration.service';
 import { SharedModule } from '../../../../shared/shared.module';
 import { OsdRecvSpeedModalComponent } from './osd-recv-speed-modal.component';
 
 describe('OsdRecvSpeedModalComponent', () => {
   let component: OsdRecvSpeedModalComponent;
   let fixture: ComponentFixture<OsdRecvSpeedModalComponent>;
+  let configurationService: ConfigurationService;
 
   configureTestBed({
     imports: [
@@ -34,7 +37,7 @@ describe('OsdRecvSpeedModalComponent', () => {
     fixture = TestBed.createComponent(OsdRecvSpeedModalComponent);
     component = fixture.componentInstance;
     fixture.detectChanges();
-
+    configurationService = TestBed.get(ConfigurationService);
     configOptions = [
       {
         name: 'osd_max_backfills',
@@ -61,12 +64,44 @@ describe('OsdRecvSpeedModalComponent', () => {
         default: 0
       }
     ];
+    spyOn(configurationService, 'filter').and.returnValue(observableOf(configOptions));
   });
 
   it('should create', () => {
     expect(component).toBeTruthy();
   });
 
+  describe('ngOnInit', () => {
+    let setPriority;
+    let setValidators;
+
+    beforeEach(() => {
+      setPriority = spyOn(component, 'setPriority').and.callThrough();
+      setValidators = spyOn(component, 'setValidators').and.callThrough();
+      component.ngOnInit();
+    });
+
+    it('should call setValidators', () => {
+      expect(setValidators).toHaveBeenCalled();
+    });
+
+    it('should get and set priority correctly', () => {
+      const defaultPriority = _.find(component.priorities, (p) => {
+        return _.isEqual(p.name, 'default');
+      });
+      expect(setPriority).toHaveBeenCalledWith(defaultPriority);
+    });
+
+    it('should set descriptions correctly', () => {
+      expect(component.priorityAttrs['osd_max_backfills'].desc).toBe('');
+      expect(component.priorityAttrs['osd_recovery_max_active'].desc).toBe('');
+      expect(component.priorityAttrs['osd_recovery_max_single_start'].desc).toBe('');
+      expect(component.priorityAttrs['osd_recovery_sleep'].desc).toBe(
+        'Time in seconds to sleep before next recovery or backfill op'
+      );
+    });
+  });
+
   describe('setPriority', () => {
     it('should prepare the form for a custom priority', () => {
       const customPriority = {
index dcb7db6985f4c92c89cd33aaf831130457a806bf..17e98894938fd5247edfdabc7b92aee8cd6fd5ae 100755 (executable)
@@ -4,8 +4,6 @@ import { FormControl, Validators } from '@angular/forms';
 import { I18n } from '@ngx-translate/i18n-polyfill';
 import * as _ from 'lodash';
 import { BsModalRef } from 'ngx-bootstrap/modal';
-import { forkJoin as observableForkJoin, of } from 'rxjs';
-import { mergeMap } from 'rxjs/operators';
 
 import { ConfigurationService } from '../../../../shared/api/configuration.service';
 import { OsdService } from '../../../../shared/api/osd.service';
@@ -76,24 +74,14 @@ export class OsdRecvSpeedModalComponent implements OnInit {
   }
 
   ngOnInit() {
-    const observables = [];
-    Object.keys(this.priorityAttrs).forEach((configOptionName) => {
-      observables.push(this.configService.get(configOptionName));
-    });
-
-    observableForkJoin(observables)
-      .pipe(
-        mergeMap((configOptions) => {
-          return of(this.getCurrentValues(configOptions));
-        })
-      )
-      .subscribe((resp) => {
-        this.detectPriority(resp.values, (priority) => {
-          this.setPriority(priority);
-        });
-        this.setDescription(resp.configOptions);
-        this.setValidators(resp.configOptions);
+    this.configService.filter(Object.keys(this.priorityAttrs)).subscribe((data: any) => {
+      const config_option_values = this.getCurrentValues(data);
+      this.detectPriority(config_option_values.values, (priority) => {
+        this.setPriority(priority);
       });
+      this.setDescription(config_option_values.configOptions);
+      this.setValidators(config_option_values.configOptions);
+    });
   }
 
   detectPriority(configOptionValues: any, callbackFn: Function) {
index 0b4133e5a409a26f2ff079436aa57afdae73dc43..9add3a5b7745bba789dba199f988a14c67999b9a 100644 (file)
@@ -51,4 +51,30 @@ describe('ConfigurationService', () => {
     expect(req.request.method).toBe('POST');
     expect(req.request.body).toEqual(configOption);
   });
+
+  it('should call bulkCreate', () => {
+    const configOptions = {
+      configOption1: { section: 'section', value: 'value' },
+      configOption2: { section: 'section', value: 'value' }
+    };
+    service.bulkCreate(configOptions).subscribe();
+    const req = httpTesting.expectOne('api/cluster_conf/');
+    expect(req.request.method).toBe('PUT');
+    expect(req.request.body).toEqual(configOptions);
+  });
+
+  it('should call filter', () => {
+    const configOptions = ['configOption1', 'configOption2', 'configOption3'];
+    service.filter(configOptions).subscribe();
+    const req = httpTesting.expectOne(
+      'api/cluster_conf/filter?names=configOption1,configOption2,configOption3'
+    );
+    expect(req.request.method).toBe('GET');
+  });
+
+  it('should call delete', () => {
+    service.delete('testOption', 'testSection').subscribe();
+    const reg = httpTesting.expectOne('api/cluster_conf/testOption?section=testSection');
+    expect(reg.request.method).toBe('DELETE');
+  });
 });
index a977a17d180e7e6c24c45bbe97197f74450c3664..3355b8fb7723886e98c0aaaa551a50208877f12f 100644 (file)
@@ -18,11 +18,19 @@ export class ConfigurationService {
     return this.http.get(`api/cluster_conf/${configOption}`);
   }
 
+  filter(configOptionNames: Array<string>) {
+    return this.http.get(`api/cluster_conf/filter?names=${configOptionNames.join(',')}`);
+  }
+
   create(configOption: ConfigFormCreateRequestModel) {
     return this.http.post('api/cluster_conf/', configOption);
   }
 
+  delete(configOption: string, section: string) {
+    return this.http.delete(`api/cluster_conf/${configOption}?section=${section}`);
+  }
+
   bulkCreate(configOptions: Object) {
-    return this.http.put('api/cluster_conf', configOptions);
+    return this.http.put('api/cluster_conf/', configOptions);
   }
 }