]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: assign flags to single OSDs 36449/head
authorTatjana Dehler <tdehler@suse.com>
Mon, 27 Jul 2020 09:33:19 +0000 (11:33 +0200)
committerTatjana Dehler <tdehler@suse.com>
Wed, 21 Oct 2020 13:11:23 +0000 (15:11 +0200)
Add the possibility to assign the flags ['noup',
'nodown', 'noin', 'noout'] to single OSDs.

Fixes: https://tracker.ceph.com/issues/40739
Signed-off-by: Tatjana Dehler <tdehler@suse.com>
16 files changed:
qa/suites/rados/dashboard/tasks/dashboard.yaml
qa/tasks/mgr/dashboard/test_osd.py
src/pybind/mgr/dashboard/controllers/osd.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/flag.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/openapi.yaml

index c958d4abd16947f70ce646e95055f6afe5e80686..9d39ff47c1982a565780743db0eba877d9885d62 100644 (file)
@@ -22,6 +22,7 @@ tasks:
         - \(OSD_HOST_DOWN\)
         - \(POOL_APP_NOT_ENABLED\)
         - \(OSDMAP_FLAGS\)
+        - \(OSD_FLAGS\)
         - pauserd,pausewr flag\(s\) set
         - Monitor daemon marked osd\.[[:digit:]]+ down, but it is still running
         - evicting unresponsive client .+
index 3747fcb164a4dab014b9eb762317b17b3bfed6eb..933a13a05f79a1287008dac96ab193e2b1d75e05 100644 (file)
@@ -234,36 +234,139 @@ class OsdTest(DashboardTestCase):
 class OsdFlagsTest(DashboardTestCase):
     def __init__(self, *args, **kwargs):
         super(OsdFlagsTest, self).__init__(*args, **kwargs)
-        self._initial_flags = sorted(  # These flags cannot be unset
-            ['sortbitwise', 'recovery_deletes', 'purged_snapdirs',
-             'pglog_hardlimit'])
+        self._initial_flags = ['sortbitwise', 'recovery_deletes', 'purged_snapdirs',
+                               'pglog_hardlimit']  # These flags cannot be unset
 
     @classmethod
-    def _get_cluster_osd_flags(cls):
-        return sorted(
-            json.loads(cls._ceph_cmd(['osd', 'dump',
-                                      '--format=json']))['flags_set'])
+    def _put_flags(cls, flags, ids=None):
+        url = '/api/osd/flags'
+        data = {'flags': flags}
 
-    @classmethod
-    def _put_flags(cls, flags):
-        cls._put('/api/osd/flags', data={'flags': flags})
-        return sorted(cls._resp.json())
+        if ids:
+            url = url + '/individual'
+            data['ids'] = ids
+
+        cls._put(url, data=data)
+        return cls._resp.json()
 
     def test_list_osd_flags(self):
         flags = self._get('/api/osd/flags')
         self.assertStatus(200)
         self.assertEqual(len(flags), 4)
-        self.assertEqual(sorted(flags), self._initial_flags)
+        self.assertCountEqual(flags, self._initial_flags)
 
     def test_add_osd_flag(self):
         flags = self._put_flags([
             'sortbitwise', 'recovery_deletes', 'purged_snapdirs', 'noout',
             'pause', 'pglog_hardlimit'
         ])
-        self.assertEqual(flags, sorted([
+        self.assertCountEqual(flags, [
             'sortbitwise', 'recovery_deletes', 'purged_snapdirs', 'noout',
             'pause', 'pglog_hardlimit'
-        ]))
+        ])
 
         # Restore flags
         self._put_flags(self._initial_flags)
+
+    def test_get_indiv_flag(self):
+        initial = self._get('/api/osd/flags/individual')
+        self.assertStatus(200)
+        self.assertSchema(initial, JList(JObj({
+            'osd': int,
+            'flags': JList(str)
+        })))
+
+        self._ceph_cmd(['osd', 'set-group', 'noout,noin', 'osd.0', 'osd.1', 'osd.2'])
+        flags_added = self._get('/api/osd/flags/individual')
+        self.assertStatus(200)
+        for osd in flags_added:
+            if osd['osd'] in [0, 1, 2]:
+                self.assertIn('noout', osd['flags'])
+                self.assertIn('noin', osd['flags'])
+                for osd_initial in initial:
+                    if osd['osd'] == osd_initial['osd']:
+                        self.assertGreater(len(osd['flags']), len(osd_initial['flags']))
+
+        self._ceph_cmd(['osd', 'unset-group', 'noout,noin', 'osd.0', 'osd.1', 'osd.2'])
+        flags_removed = self._get('/api/osd/flags/individual')
+        self.assertStatus(200)
+        for osd in flags_removed:
+            if osd['osd'] in [0, 1, 2]:
+                self.assertNotIn('noout', osd['flags'])
+                self.assertNotIn('noin', osd['flags'])
+
+    def test_add_indiv_flag(self):
+        flags_update = {'noup': None, 'nodown': None, 'noin': None, 'noout': True}
+        svc_id = 0
+
+        resp = self._put_flags(flags_update, [svc_id])
+        self._check_indiv_flags_resp(resp, [svc_id], ['noout'], [], ['noup', 'nodown', 'noin'])
+        self._check_indiv_flags_osd([svc_id], ['noout'], ['noup', 'nodown', 'noin'])
+
+        self._ceph_cmd(['osd', 'unset-group', 'noout', 'osd.{}'.format(svc_id)])
+
+    def test_add_multiple_indiv_flags(self):
+        flags_update = {'noup': None, 'nodown': None, 'noin': True, 'noout': True}
+        svc_id = 0
+
+        resp = self._put_flags(flags_update, [svc_id])
+        self._check_indiv_flags_resp(resp, [svc_id], ['noout', 'noin'], [], ['noup', 'nodown'])
+        self._check_indiv_flags_osd([svc_id], ['noout', 'noin'], ['noup', 'nodown'])
+
+        self._ceph_cmd(['osd', 'unset-group', 'noout,noin', 'osd.{}'.format(svc_id)])
+
+    def test_add_multiple_indiv_flags_multiple_osds(self):
+        flags_update = {'noup': None, 'nodown': None, 'noin': True, 'noout': True}
+        svc_id = [0, 1, 2]
+
+        resp = self._put_flags(flags_update, svc_id)
+        self._check_indiv_flags_resp(resp, svc_id, ['noout', 'noin'], [], ['noup', 'nodown'])
+        self._check_indiv_flags_osd([svc_id], ['noout', 'noin'], ['noup', 'nodown'])
+
+        self._ceph_cmd(['osd', 'unset-group', 'noout,noin', 'osd.0', 'osd.1', 'osd.2'])
+
+    def test_remove_indiv_flag(self):
+        flags_update = {'noup': None, 'nodown': None, 'noin': None, 'noout': False}
+        svc_id = 0
+        self._ceph_cmd(['osd', 'set-group', 'noout', 'osd.{}'.format(svc_id)])
+
+        resp = self._put_flags(flags_update, [svc_id])
+        self._check_indiv_flags_resp(resp, [svc_id], [], ['noout'], ['noup', 'nodown', 'noin'])
+        self._check_indiv_flags_osd([svc_id], [], ['noup', 'nodown', 'noin', 'noout'])
+
+    def test_remove_multiple_indiv_flags(self):
+        flags_update = {'noup': None, 'nodown': None, 'noin': False, 'noout': False}
+        svc_id = 0
+        self._ceph_cmd(['osd', 'set-group', 'noout,noin', 'osd.{}'.format(svc_id)])
+
+        resp = self._put_flags(flags_update, [svc_id])
+        self._check_indiv_flags_resp(resp, [svc_id], [], ['noout', 'noin'], ['noup', 'nodown'])
+        self._check_indiv_flags_osd([svc_id], [], ['noout', 'noin', 'noup', 'nodown'])
+
+    def test_remove_multiple_indiv_flags_multiple_osds(self):
+        flags_update = {'noup': None, 'nodown': None, 'noin': False, 'noout': False}
+        svc_id = [0, 1, 2]
+        self._ceph_cmd(['osd', 'unset-group', 'noout,noin', 'osd.0', 'osd.1', 'osd.2'])
+
+        resp = self._put_flags(flags_update, svc_id)
+        self._check_indiv_flags_resp(resp, svc_id, [], ['noout', 'noin'], ['noup', 'nodown'])
+        self._check_indiv_flags_osd([svc_id], [], ['noout', 'noin', 'noup', 'nodown'])
+
+    def _check_indiv_flags_resp(self, resp, ids, added, removed, ignored):
+        self.assertStatus(200)
+        self.assertCountEqual(resp['ids'], ids)
+        self.assertCountEqual(resp['added'], added)
+        self.assertCountEqual(resp['removed'], removed)
+
+        for flag in ignored:
+            self.assertNotIn(flag, resp['added'])
+            self.assertNotIn(flag, resp['removed'])
+
+    def _check_indiv_flags_osd(self, ids, activated_flags, deactivated_flags):
+        osds = json.loads(self._ceph_cmd(['osd', 'dump', '--format=json']))['osds']
+        for osd in osds:
+            if osd['osd'] in ids:
+                for flag in activated_flags:
+                    self.assertIn(flag, osd['state'])
+                for flag in deactivated_flags:
+                    self.assertNotIn(flag, osd['state'])
index 91d1126fa4064c18ac4d3e11feb656d811ba857a..ceee0a245171e7652dc382e0c6c1803fa5a6ffac 100644 (file)
@@ -35,6 +35,17 @@ EXPORT_FLAGS_SCHEMA = {
     "list_of_flags": ([str], "")
 }
 
+EXPORT_INDIV_FLAGS_SCHEMA = {
+    "added": ([str], "List of added flags"),
+    "removed": ([str], "List of removed flags"),
+    "ids": ([int], "List of updated OSDs")
+}
+
+EXPORT_INDIV_FLAGS_GET_SCHEMA = {
+    "osd": (int, "OSD ID"),
+    "flags": ([str], "List of active flags")
+}
+
 
 def osd_task(name, metadata, wait_for=2.0):
     return Task("osd/{}".format(name), metadata, wait_for)
@@ -381,11 +392,29 @@ class OsdFlagsController(RESTController):
                 set(enabled_flags) - {'pauserd', 'pausewr'} | {'pause'})
         return sorted(enabled_flags)
 
+    @staticmethod
+    def _update_flags(action, flags, ids=None):
+        if ids:
+            if flags:
+                ids = list(map(str, ids))
+                CephService.send_command('mon', 'osd ' + action, who=ids,
+                                         flags=','.join(flags))
+        else:
+            for flag in flags:
+                CephService.send_command('mon', 'osd ' + action, '', key=flag)
+
     @EndpointDoc("Display OSD Flags",
                  responses={200: EXPORT_FLAGS_SCHEMA})
     def list(self):
         return self._osd_flags()
 
+    @EndpointDoc('Sets OSD flags for the entire cluster.',
+                 parameters={
+                     'flags': ([str], 'List of flags to set. The flags `recovery_deletes`, '
+                                      '`sortbitwise` and `pglog_hardlimit` cannot be unset. '
+                                      'Additionally `purged_snapshots` cannot even be set.')
+                 },
+                 responses={200: EXPORT_FLAGS_SCHEMA})
     def bulk_set(self, flags):
         """
         The `recovery_deletes`, `sortbitwise` and `pglog_hardlimit` flags cannot be unset.
@@ -398,10 +427,71 @@ class OsdFlagsController(RESTController):
         data = set(flags)
         added = data - enabled_flags
         removed = enabled_flags - data
-        for flag in added:
-            CephService.send_command('mon', 'osd set', '', key=flag)
-        for flag in removed:
-            CephService.send_command('mon', 'osd unset', '', key=flag)
+
+        self._update_flags('set', added)
+        self._update_flags('unset', removed)
+
         logger.info('Changed OSD flags: added=%s removed=%s', added, removed)
 
         return sorted(enabled_flags - removed | added)
+
+    @Endpoint('PUT', 'individual')
+    @UpdatePermission
+    @EndpointDoc('Sets OSD flags for a subset of individual OSDs.',
+                 parameters={
+                     'flags': ({'noout': (bool, 'Sets/unsets `noout`', True, None),
+                                'noin': (bool, 'Sets/unsets `noin`', True, None),
+                                'noup': (bool, 'Sets/unsets `noup`', True, None),
+                                'nodown': (bool, 'Sets/unsets `nodown`', True, None)},
+                               'Directory of flags to set or unset. The flags '
+                               '`noin`, `noout`, `noup` and `nodown` are going to '
+                               'be considered only.'),
+                     'ids': ([int], 'List of OSD ids the flags should be applied '
+                                    'to.')
+                 },
+                 responses={200: EXPORT_INDIV_FLAGS_SCHEMA})
+    def set_individual(self, flags, ids):
+        """
+        Updates flags (`noout`, `noin`, `nodown`, `noup`) for an individual
+        subset of OSDs.
+        """
+        assert isinstance(flags, dict)
+        assert isinstance(ids, list)
+        assert all(isinstance(id, int) for id in ids)
+
+        # These are to only flags that can be applied to an OSD individually.
+        all_flags = {'noin', 'noout', 'nodown', 'noup'}
+        added = set()
+        removed = set()
+        for flag, activated in flags.items():
+            if flag in all_flags:
+                if activated is not None:
+                    if activated:
+                        added.add(flag)
+                    else:
+                        removed.add(flag)
+
+        self._update_flags('set-group', added, ids)
+        self._update_flags('unset-group', removed, ids)
+
+        logger.error('Changed individual OSD flags: added=%s removed=%s for ids=%s',
+                     added, removed, ids)
+
+        return {'added': sorted(added),
+                'removed': sorted(removed),
+                'ids': ids}
+
+    @Endpoint('GET', 'individual')
+    @ReadPermission
+    @EndpointDoc('Displays individual OSD flags',
+                 responses={200: EXPORT_INDIV_FLAGS_GET_SCHEMA})
+    def get_individual(self):
+        osd_map = mgr.get('osd_map')['osds']
+        resp = []
+
+        for osd in osd_map:
+            resp.append({
+                'osd': osd['osd'],
+                'flags': osd['state']
+            })
+        return resp
index eb73ccc49aba51b746197f35f1e5090e31169d15..67ffc5e050cfc7de23493ad52f898765c8546b97 100644 (file)
@@ -33,6 +33,7 @@ import { OsdCreationPreviewModalComponent } from './osd/osd-creation-preview-mod
 import { OsdDetailsComponent } from './osd/osd-details/osd-details.component';
 import { OsdDevicesSelectionGroupsComponent } from './osd/osd-devices-selection-groups/osd-devices-selection-groups.component';
 import { OsdDevicesSelectionModalComponent } from './osd/osd-devices-selection-modal/osd-devices-selection-modal.component';
+import { OsdFlagsIndivModalComponent } from './osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component';
 import { OsdFlagsModalComponent } from './osd/osd-flags-modal/osd-flags-modal.component';
 import { OsdFormComponent } from './osd/osd-form/osd-form.component';
 import { OsdListComponent } from './osd/osd-list/osd-list.component';
@@ -106,7 +107,8 @@ import { TelemetryComponent } from './telemetry/telemetry.component';
     ServiceDaemonListComponent,
     TelemetryComponent,
     PrometheusTabsComponent,
-    ServiceFormComponent
+    ServiceFormComponent,
+    OsdFlagsIndivModalComponent
   ]
 })
 export class ClusterModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.html
new file mode 100644 (file)
index 0000000..c392b83
--- /dev/null
@@ -0,0 +1,52 @@
+<cd-modal [modalRef]="activeModal">
+  <ng-container class="modal-title"
+                i18n>Individual OSD Flags</ng-container>
+
+  <ng-container class="modal-content">
+    <form name="osdFlagsForm"
+          #formDir="ngForm"
+          [formGroup]="osdFlagsForm"
+          novalidate>
+      <div class="modal-body osd-modal">
+        <div class="custom-control custom-checkbox"
+             *ngFor="let flag of flags; let last = last">
+          <input class="custom-control-input"
+                 type="checkbox"
+                 [checked]="flag.value"
+                 [indeterminate]="flag.indeterminate"
+                 (change)="changeValue(flag)"
+                 [name]="flag.code"
+                 [id]="flag.code">
+          <label class="custom-control-label"
+                 [for]="flag.code"
+                 ng-class="['tc_' + key]">
+            <strong>{{ flag.name }}</strong>
+            <span class="badge badge-hdd ml-2"
+                  [ngbTooltip]="clusterWideTooltip"
+                  *ngIf="flag.clusterWide"
+                  i18n>Cluster-wide</span>
+            <br>
+            <span class="form-text text-muted">{{ flag.description }}</span>
+          </label>
+          <hr class="m-1"
+              *ngIf="!last">
+        </div>
+      </div>
+
+      <div class="modal-footer">
+        <button type="button"
+                class="btn btn-light"
+                (click)="resetSelection()"
+                i18n>Restore previous selection</button>
+        <cd-submit-button *ngIf="permissions.osd.update"
+                          (submitAction)="submitAction()"
+                          [form]="osdFlagsForm"
+                          i18n>Submit</cd-submit-button>
+        <cd-back-button [back]="activeModal.close"
+                        name="Cancel"
+                        i18n-name>
+        </cd-back-button>
+      </div>
+    </form>
+  </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.spec.ts
new file mode 100644 (file)
index 0000000..c2be010
--- /dev/null
@@ -0,0 +1,351 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+
+import { NgbActiveModal, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { of as observableOf } from 'rxjs';
+
+import { configureTestBed } from '../../../../../testing/unit-test-helper';
+import { OsdService } from '../../../../shared/api/osd.service';
+import { NotificationType } from '../../../../shared/enum/notification-type.enum';
+import { Flag } from '../../../../shared/models/flag';
+import { NotificationService } from '../../../../shared/services/notification.service';
+import { SharedModule } from '../../../../shared/shared.module';
+import { OsdFlagsIndivModalComponent } from './osd-flags-indiv-modal.component';
+
+describe('OsdFlagsIndivModalComponent', () => {
+  let component: OsdFlagsIndivModalComponent;
+  let fixture: ComponentFixture<OsdFlagsIndivModalComponent>;
+  let httpTesting: HttpTestingController;
+  let osdService: OsdService;
+
+  configureTestBed({
+    imports: [
+      HttpClientTestingModule,
+      ReactiveFormsModule,
+      SharedModule,
+      ToastrModule.forRoot(),
+      NgbTooltipModule
+    ],
+    declarations: [OsdFlagsIndivModalComponent],
+    providers: [NgbActiveModal]
+  });
+
+  beforeEach(() => {
+    httpTesting = TestBed.inject(HttpTestingController);
+    fixture = TestBed.createComponent(OsdFlagsIndivModalComponent);
+    component = fixture.componentInstance;
+    osdService = TestBed.inject(OsdService);
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  describe('getActivatedIndivFlags', () => {
+    function checkFlagsCount(
+      counts: { [key: string]: number },
+      expected: { [key: string]: number }
+    ) {
+      Object.entries(expected).forEach(([expectedKey, expectedValue]) => {
+        expect(counts[expectedKey]).toBe(expectedValue);
+      });
+    }
+
+    it('should count correctly if no flag has been set', () => {
+      component.selected = generateSelected();
+      const countedFlags = component.getActivatedIndivFlags();
+      checkFlagsCount(countedFlags, { noup: 0, nodown: 0, noin: 0, noout: 0 });
+    });
+
+    it('should count correctly if some of the flags have been set', () => {
+      component.selected = generateSelected([['noin'], ['noin', 'noout'], ['nodown']]);
+      const countedFlags = component.getActivatedIndivFlags();
+      checkFlagsCount(countedFlags, { noup: 0, nodown: 1, noin: 2, noout: 1 });
+    });
+  });
+
+  describe('changeValue', () => {
+    it('should change value correctly and set indeterminate to false', () => {
+      const testFlag = component.flags[0];
+      const value = testFlag.value;
+      component.changeValue(testFlag);
+      expect(testFlag.value).toBe(!value);
+      expect(testFlag.indeterminate).toBeFalsy();
+    });
+  });
+
+  describe('resetSelection', () => {
+    it('should set a new flags object by deep cloning the initial selection', () => {
+      component.resetSelection();
+      expect(component.flags === component.initialSelection).toBeFalsy();
+    });
+  });
+
+  describe('OSD single-select', () => {
+    beforeEach(() => {
+      component.selected = [{ osd: 0 }];
+    });
+
+    describe('ngOnInit', () => {
+      it('should clone flags as initial selection', () => {
+        expect(component.flags === component.initialSelection).toBeFalsy();
+      });
+
+      it('should initialize form correctly if no individual and global flags are set', () => {
+        component.selected[0]['state'] = ['exists', 'up'];
+        spyOn(osdService, 'getFlags').and.callFake(() => observableOf([]));
+        fixture.detectChanges();
+        checkFlags(component.flags);
+      });
+
+      it('should initialize form correctly if individual but no global flags are set', () => {
+        component.selected[0]['state'] = ['exists', 'noout', 'up'];
+        spyOn(osdService, 'getFlags').and.callFake(() => observableOf([]));
+        fixture.detectChanges();
+        const expected = {
+          noout: { value: true, clusterWide: false, indeterminate: false }
+        };
+        checkFlags(component.flags, expected);
+      });
+
+      it('should initialize form correctly if multiple individual but no global flags are set', () => {
+        component.selected[0]['state'] = ['exists', 'noin', 'noout', 'up'];
+        spyOn(osdService, 'getFlags').and.callFake(() => observableOf([]));
+        fixture.detectChanges();
+        const expected = {
+          noout: { value: true, clusterWide: false, indeterminate: false },
+          noin: { value: true, clusterWide: false, indeterminate: false }
+        };
+        checkFlags(component.flags, expected);
+      });
+
+      it('should initialize form correctly if no individual but global flags are set', () => {
+        component.selected[0]['state'] = ['exists', 'up'];
+        spyOn(osdService, 'getFlags').and.callFake(() => observableOf(['noout']));
+        fixture.detectChanges();
+        const expected = {
+          noout: { value: false, clusterWide: true, indeterminate: false }
+        };
+        checkFlags(component.flags, expected);
+      });
+    });
+
+    describe('submitAction', () => {
+      let notificationType: NotificationType;
+      let notificationService: NotificationService;
+      let bsModalRef: NgbActiveModal;
+      let flags: object;
+
+      beforeEach(() => {
+        notificationService = TestBed.inject(NotificationService);
+        spyOn(notificationService, 'show').and.callFake((type) => {
+          notificationType = type;
+        });
+        bsModalRef = TestBed.inject(NgbActiveModal);
+        spyOn(bsModalRef, 'close').and.callThrough();
+        flags = {
+          nodown: false,
+          noin: false,
+          noout: false,
+          noup: false
+        };
+      });
+
+      it('should submit an activated flag', () => {
+        const code = component.flags[0].code;
+        component.flags[0].value = true;
+        component.submitAction();
+        flags[code] = true;
+
+        const req = httpTesting.expectOne('api/osd/flags/individual');
+        req.flush({ flags, ids: [0] });
+        expect(req.request.body).toEqual({ flags, ids: [0] });
+        expect(notificationType).toBe(NotificationType.success);
+        expect(component.activeModal.close).toHaveBeenCalledTimes(1);
+      });
+
+      it('should submit multiple flags', () => {
+        const codes = [component.flags[0].code, component.flags[1].code];
+        component.flags[0].value = true;
+        component.flags[1].value = true;
+        component.submitAction();
+        flags[codes[0]] = true;
+        flags[codes[1]] = true;
+
+        const req = httpTesting.expectOne('api/osd/flags/individual');
+        req.flush({ flags, ids: [0] });
+        expect(req.request.body).toEqual({ flags, ids: [0] });
+        expect(notificationType).toBe(NotificationType.success);
+        expect(component.activeModal.close).toHaveBeenCalledTimes(1);
+      });
+
+      it('should hide modal if request fails', () => {
+        component.flags = [];
+        component.submitAction();
+        const req = httpTesting.expectOne('api/osd/flags/individual');
+        req.flush([], { status: 500, statusText: 'failure' });
+        expect(notificationService.show).toHaveBeenCalledTimes(0);
+        expect(component.activeModal.close).toHaveBeenCalledTimes(1);
+      });
+    });
+  });
+
+  describe('OSD multi-select', () => {
+    describe('ngOnInit', () => {
+      it('should initialize form correctly if same individual and no global flags are set', () => {
+        component.selected = generateSelected([['noin'], ['noin'], ['noin']]);
+        spyOn(osdService, 'getFlags').and.callFake(() => observableOf([]));
+        fixture.detectChanges();
+        const expected = {
+          noin: { value: true, clusterWide: false, indeterminate: false }
+        };
+        checkFlags(component.flags, expected);
+      });
+
+      it('should initialize form correctly if different individual and no global flags are set', () => {
+        component.selected = generateSelected([['noin'], ['noout'], ['noin']]);
+        spyOn(osdService, 'getFlags').and.callFake(() => observableOf([]));
+        fixture.detectChanges();
+        const expected = {
+          noin: { value: false, clusterWide: false, indeterminate: true },
+          noout: { value: false, clusterWide: false, indeterminate: true }
+        };
+        checkFlags(component.flags, expected);
+      });
+
+      it('should initialize form correctly if different and same individual and no global flags are set', () => {
+        component.selected = generateSelected([
+          ['noin', 'nodown'],
+          ['noout', 'nodown'],
+          ['noin', 'nodown']
+        ]);
+        spyOn(osdService, 'getFlags').and.callFake(() => observableOf([]));
+        fixture.detectChanges();
+        const expected = {
+          noin: { value: false, clusterWide: false, indeterminate: true },
+          noout: { value: false, clusterWide: false, indeterminate: true },
+          nodown: { value: true, clusterWide: false, indeterminate: false }
+        };
+        checkFlags(component.flags, expected);
+      });
+
+      it('should initialize form correctly if a flag is set for all OSDs individually and globally', () => {
+        component.selected = generateSelected([
+          ['noin', 'nodown'],
+          ['noout', 'nodown'],
+          ['noin', 'nodown']
+        ]);
+        spyOn(osdService, 'getFlags').and.callFake(() => observableOf(['noout']));
+        fixture.detectChanges();
+        const expected = {
+          noin: { value: false, clusterWide: false, indeterminate: true },
+          noout: { value: false, clusterWide: true, indeterminate: true },
+          nodown: { value: true, clusterWide: false, indeterminate: false }
+        };
+        checkFlags(component.flags, expected);
+      });
+
+      it('should initialize form correctly if different individual and global flags are set', () => {
+        component.selected = generateSelected([
+          ['noin', 'nodown', 'noout'],
+          ['noout', 'nodown'],
+          ['noin', 'nodown', 'noout']
+        ]);
+        spyOn(osdService, 'getFlags').and.callFake(() => observableOf(['noout']));
+        fixture.detectChanges();
+        const expected = {
+          noin: { value: false, clusterWide: false, indeterminate: true },
+          noout: { value: true, clusterWide: true, indeterminate: false },
+          nodown: { value: true, clusterWide: false, indeterminate: false }
+        };
+        checkFlags(component.flags, expected);
+      });
+    });
+
+    describe('submitAction', () => {
+      let notificationType: NotificationType;
+      let notificationService: NotificationService;
+      let bsModalRef: NgbActiveModal;
+      let flags: object;
+
+      beforeEach(() => {
+        notificationService = TestBed.inject(NotificationService);
+        spyOn(notificationService, 'show').and.callFake((type) => {
+          notificationType = type;
+        });
+        bsModalRef = TestBed.inject(NgbActiveModal);
+        spyOn(bsModalRef, 'close').and.callThrough();
+        flags = {
+          nodown: false,
+          noin: false,
+          noout: false,
+          noup: false
+        };
+      });
+
+      it('should submit an activated flag for multiple OSDs', () => {
+        component.selected = generateSelected();
+        const code = component.flags[0].code;
+        const submittedIds = [0, 1, 2];
+        component.flags[0].value = true;
+        component.submitAction();
+        flags[code] = true;
+
+        const req = httpTesting.expectOne('api/osd/flags/individual');
+        req.flush({ flags, ids: submittedIds });
+        expect(req.request.body).toEqual({ flags, ids: submittedIds });
+        expect(notificationType).toBe(NotificationType.success);
+        expect(component.activeModal.close).toHaveBeenCalledTimes(1);
+      });
+
+      it('should submit multiple flags for multiple OSDs', () => {
+        component.selected = generateSelected();
+        const codes = [component.flags[0].code, component.flags[1].code];
+        const submittedIds = [0, 1, 2];
+        component.flags[0].value = true;
+        component.flags[1].value = true;
+        component.submitAction();
+        flags[codes[0]] = true;
+        flags[codes[1]] = true;
+
+        const req = httpTesting.expectOne('api/osd/flags/individual');
+        req.flush({ flags, ids: submittedIds });
+        expect(req.request.body).toEqual({ flags, ids: submittedIds });
+        expect(notificationType).toBe(NotificationType.success);
+        expect(component.activeModal.close).toHaveBeenCalledTimes(1);
+      });
+    });
+  });
+
+  function checkFlags(flags: Flag[], expected: object = {}) {
+    flags.forEach((flag) => {
+      let value = false;
+      let clusterWide = false;
+      let indeterminate = false;
+      if (Object.keys(expected).includes(flag.code)) {
+        value = expected[flag.code]['value'];
+        clusterWide = expected[flag.code]['clusterWide'];
+        indeterminate = expected[flag.code]['indeterminate'];
+      }
+      expect(flag.value).toBe(value);
+      expect(flag.clusterWide).toBe(clusterWide);
+      expect(flag.indeterminate).toBe(indeterminate);
+    });
+  }
+
+  function generateSelected(flags: string[][] = []) {
+    const defaultFlags = ['exists', 'up'];
+    const osds = [];
+    const count = flags.length || 3;
+    for (let i = 0; i < count; i++) {
+      const osd = {
+        osd: i,
+        state: defaultFlags.concat(flags[i]) || defaultFlags
+      };
+      osds.push(osd);
+    }
+    return osds;
+  }
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-indiv-modal/osd-flags-indiv-modal.component.ts
new file mode 100644 (file)
index 0000000..055ac72
--- /dev/null
@@ -0,0 +1,132 @@
+import { Component, OnInit } from '@angular/core';
+import { FormGroup } from '@angular/forms';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+
+import { OsdService } from '../../../../shared/api/osd.service';
+import { NotificationType } from '../../../../shared/enum/notification-type.enum';
+import { Flag } from '../../../../shared/models/flag';
+import { Permissions } from '../../../../shared/models/permissions';
+import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
+import { NotificationService } from '../../../../shared/services/notification.service';
+
+@Component({
+  selector: 'cd-osd-flags-indiv-modal',
+  templateUrl: './osd-flags-indiv-modal.component.html',
+  styleUrls: ['./osd-flags-indiv-modal.component.scss']
+})
+export class OsdFlagsIndivModalComponent implements OnInit {
+  permissions: Permissions;
+  selected: object[];
+  initialSelection: Flag[] = [];
+  osdFlagsForm = new FormGroup({});
+  flags: Flag[] = [
+    {
+      code: 'noup',
+      name: $localize`No Up`,
+      description: $localize`OSDs are not allowed to start`,
+      value: false,
+      clusterWide: false,
+      indeterminate: false
+    },
+    {
+      code: 'nodown',
+      name: $localize`No Down`,
+      description: $localize`OSD failure reports are being ignored, such that the monitors will not mark OSDs down`,
+      value: false,
+      clusterWide: false,
+      indeterminate: false
+    },
+    {
+      code: 'noin',
+      name: $localize`No In`,
+      description: $localize`OSDs that were previously marked out will not be marked back in when they start`,
+      value: false,
+      clusterWide: false,
+      indeterminate: false
+    },
+    {
+      code: 'noout',
+      name: $localize`No Out`,
+      description: $localize`OSDs will not automatically be marked out after the configured interval`,
+      value: false,
+      clusterWide: false,
+      indeterminate: false
+    }
+  ];
+  clusterWideTooltip: string = $localize`The flag has been enabled for the entire cluster.`;
+
+  constructor(
+    public activeModal: NgbActiveModal,
+    private authStorageService: AuthStorageService,
+    private osdService: OsdService,
+    private notificationService: NotificationService
+  ) {
+    this.permissions = this.authStorageService.getPermissions();
+  }
+
+  ngOnInit() {
+    const osdCount = this.selected.length;
+    this.osdService.getFlags().subscribe((clusterWideFlags: string[]) => {
+      const activatedIndivFlags = this.getActivatedIndivFlags();
+      this.flags.forEach((flag) => {
+        const flagCount = activatedIndivFlags[flag.code];
+        if (clusterWideFlags.includes(flag.code)) {
+          flag.clusterWide = true;
+        }
+
+        if (flagCount === osdCount) {
+          flag.value = true;
+        } else if (flagCount > 0) {
+          flag.indeterminate = true;
+        }
+      });
+      this.initialSelection = _.cloneDeep(this.flags);
+    });
+  }
+
+  getActivatedIndivFlags(): { [flag: string]: number } {
+    const flagsCount = {};
+    this.flags.forEach((flag) => {
+      flagsCount[flag.code] = 0;
+    });
+
+    [].concat(...this.selected.map((osd) => osd['state'])).map((activatedFlag) => {
+      if (Object.keys(flagsCount).includes(activatedFlag)) {
+        flagsCount[activatedFlag] = flagsCount[activatedFlag] + 1;
+      }
+    });
+    return flagsCount;
+  }
+
+  changeValue(flag: Flag) {
+    flag.value = !flag.value;
+    flag.indeterminate = false;
+  }
+
+  resetSelection() {
+    this.flags = _.cloneDeep(this.initialSelection);
+  }
+
+  submitAction() {
+    const activeFlags = {};
+    this.flags.forEach((flag) => {
+      if (flag.indeterminate) {
+        activeFlags[flag.code] = null;
+      } else {
+        activeFlags[flag.code] = flag.value;
+      }
+    });
+    const selectedIds = this.selected.map((selection) => selection['osd']);
+    this.osdService.updateIndividualFlags(activeFlags, selectedIds).subscribe(
+      () => {
+        this.notificationService.show(NotificationType.success, $localize`Updated OSD Flags`);
+        this.activeModal.close();
+      },
+      () => {
+        this.activeModal.close();
+      }
+    );
+  }
+}
index c8111e02612e0193e49c44c98a84dababd5c5df9..fe236902e978128fbdfb681bf758aeaef4fe9a1c 100644 (file)
   <strong>{{ actionDescription }}</strong> if you proceed.</ng-container>
 </ng-template>
 
+<ng-template #flagsTpl
+             let-row="row">
+  <span *ngFor="let flag of row.cdClusterFlags;"
+        class="badge badge-hdd mr-1">{{ flag }}</span>
+  <span *ngFor="let flag of row.cdIndivFlags;"
+        class="badge badge-info mr-1">{{ flag }}</span>
+</ng-template>
+
 <ng-template #osdUsageTpl
              let-row="row">
   <cd-usage-bar [total]="row.stats.stat_bytes"
index 3b921789415530bd5d5bc700f220e64f8e5511e3..0a1d77847f3dc51895c4556b4212f27e94b81af7 100644 (file)
@@ -139,6 +139,7 @@ describe('OsdListComponent', () => {
 
   describe('getOsdList', () => {
     let osds: any[];
+    let flagsSpy: jasmine.Spy;
 
     const createOsd = (n: number) =>
       <Record<string, any>>{
@@ -169,6 +170,7 @@ describe('OsdListComponent', () => {
 
     beforeEach(() => {
       spyOn(osdService, 'getList').and.callFake(() => of(osds));
+      flagsSpy = spyOn(osdService, 'getFlags').and.callFake(() => of([]));
       osds = [createOsd(1), createOsd(2), createOsd(3)];
       component.getOsdList();
     });
@@ -218,6 +220,42 @@ describe('OsdListComponent', () => {
       expectAttributeOnEveryOsd('cdIsBinary');
       expect(component.osds[0].cdIsBinary).toBe(true);
     });
+
+    it('should return valid individual flags only', () => {
+      const osd1 = createOsd(1);
+      const osd2 = createOsd(2);
+      osd1.state = ['noup', 'exists', 'up'];
+      osd2.state = ['noup', 'exists', 'up', 'noin'];
+      osds = [osd1, osd2];
+      component.getOsdList();
+
+      expect(component.osds[0].cdIndivFlags).toStrictEqual(['noup']);
+      expect(component.osds[1].cdIndivFlags).toStrictEqual(['noup', 'noin']);
+    });
+
+    it('should not fail on empty individual flags list', () => {
+      expect(component.osds[0].cdIndivFlags).toStrictEqual([]);
+    });
+
+    it('should not return disabled cluster-wide flags', () => {
+      flagsSpy.and.callFake(() => of(['noout', 'nodown', 'sortbitwise']));
+      component.getOsdList();
+      expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
+
+      flagsSpy.and.callFake(() => of(['noout', 'purged_snapdirs', 'nodown']));
+      component.getOsdList();
+      expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
+
+      flagsSpy.and.callFake(() => of(['recovery_deletes', 'noout', 'pglog_hardlimit', 'nodown']));
+      component.getOsdList();
+      expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
+    });
+
+    it('should not fail on empty cluster-wide flags list', () => {
+      flagsSpy.and.callFake(() => of([]));
+      component.getOsdList();
+      expect(component.osds[0].cdClusterFlags).toStrictEqual([]);
+    });
   });
 
   describe('show osd actions as defined', () => {
@@ -274,6 +312,7 @@ describe('OsdListComponent', () => {
         actions: [
           'Create',
           'Edit',
+          'Flags',
           'Scrub',
           'Deep Scrub',
           'Reweight',
@@ -291,6 +330,7 @@ describe('OsdListComponent', () => {
         actions: [
           'Create',
           'Edit',
+          'Flags',
           'Scrub',
           'Deep Scrub',
           'Reweight',
@@ -316,6 +356,7 @@ describe('OsdListComponent', () => {
       'update,delete': {
         actions: [
           'Edit',
+          'Flags',
           'Scrub',
           'Deep Scrub',
           'Reweight',
@@ -330,7 +371,16 @@ describe('OsdListComponent', () => {
         primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
       },
       update: {
-        actions: ['Edit', 'Scrub', 'Deep Scrub', 'Reweight', 'Mark Out', 'Mark In', 'Mark Down'],
+        actions: [
+          'Edit',
+          'Flags',
+          'Scrub',
+          'Deep Scrub',
+          'Reweight',
+          'Mark Out',
+          'Mark In',
+          'Mark Down'
+        ],
         primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
       },
       delete: {
index f42e9f15842bf8c84b4c4e3c90c049913674c6db..a0e61cb5db96839eeeb4895caae0ee7c0774ce23 100644 (file)
@@ -30,6 +30,7 @@ import { ModalService } from '../../../../shared/services/modal.service';
 import { NotificationService } from '../../../../shared/services/notification.service';
 import { TaskWrapperService } from '../../../../shared/services/task-wrapper.service';
 import { URLBuilderService } from '../../../../shared/services/url-builder.service';
+import { OsdFlagsIndivModalComponent } from '../osd-flags-indiv-modal/osd-flags-indiv-modal.component';
 import { OsdFlagsModalComponent } from '../osd-flags-modal/osd-flags-modal.component';
 import { OsdPgScrubModalComponent } from '../osd-pg-scrub-modal/osd-pg-scrub-modal.component';
 import { OsdRecvSpeedModalComponent } from '../osd-recv-speed-modal/osd-recv-speed-modal.component';
@@ -57,6 +58,8 @@ export class OsdListComponent extends ListWithDetails implements OnInit {
   safeToDestroyBodyTpl: TemplateRef<any>;
   @ViewChild('deleteOsdExtraTpl')
   deleteOsdExtraTpl: TemplateRef<any>;
+  @ViewChild('flagsTpl', { static: true })
+  flagsTpl: TemplateRef<any>;
 
   permissions: Permissions;
   tableActions: CdTableAction[];
@@ -67,6 +70,13 @@ export class OsdListComponent extends ListWithDetails implements OnInit {
 
   selection = new CdTableSelection();
   osds: any[] = [];
+  disabledFlags: string[] = [
+    'sortbitwise',
+    'purged_snapdirs',
+    'recovery_deletes',
+    'pglog_hardlimit'
+  ];
+  indivFlagNames: string[] = ['noup', 'nodown', 'noin', 'noout'];
 
   orchStatus: OrchestratorStatus;
   actionOrchFeatures = {
@@ -115,6 +125,13 @@ export class OsdListComponent extends ListWithDetails implements OnInit {
         icon: Icons.edit,
         click: () => this.editAction()
       },
+      {
+        name: this.actionLabels.FLAGS,
+        permission: 'update',
+        icon: Icons.flag,
+        click: () => this.configureFlagsIndivAction(),
+        disable: () => !this.hasOsdSelected
+      },
       {
         name: this.actionLabels.SCRUB,
         permission: 'update',
@@ -290,6 +307,11 @@ export class OsdListComponent extends ListWithDetails implements OnInit {
         flexGrow: 1,
         pipe: this.dimlessBinaryPipe
       },
+      {
+        prop: 'state',
+        name: $localize`Flags`,
+        cellTemplate: this.flagsTpl
+      },
       { prop: 'stats.usage', name: $localize`Usage`, cellTemplate: this.osdUsageTpl },
       {
         prop: 'stats_history.out_bytes',
@@ -371,13 +393,16 @@ export class OsdListComponent extends ListWithDetails implements OnInit {
   }
 
   getOsdList() {
-    this.osdService.getList().subscribe((data: any[]) => {
-      this.osds = data.map((osd) => {
+    const observables = [this.osdService.getList(), this.osdService.getFlags()];
+    observableForkJoin(observables).subscribe((resp: [any[], string[]]) => {
+      this.osds = resp[0].map((osd) => {
         osd.collectedStates = OsdListComponent.collectStates(osd);
         osd.stats_history.out_bytes = osd.stats_history.op_out_bytes.map((i: string) => i[1]);
         osd.stats_history.in_bytes = osd.stats_history.op_in_bytes.map((i: string) => i[1]);
         osd.stats.usage = osd.stats.stat_bytes_used / osd.stats.stat_bytes;
         osd.cdIsBinary = true;
+        osd.cdIndivFlags = osd.state.filter((f: string) => this.indivFlagNames.includes(f));
+        osd.cdClusterFlags = resp[1].filter((f: string) => !this.disabledFlags.includes(f));
         return osd;
       });
     });
@@ -427,6 +452,13 @@ export class OsdListComponent extends ListWithDetails implements OnInit {
     this.bsModalRef = this.modalService.show(OsdFlagsModalComponent);
   }
 
+  configureFlagsIndivAction() {
+    const initialState = {
+      selected: this.getSelectedOsds()
+    };
+    this.bsModalRef = this.modalService.show(OsdFlagsIndivModalComponent, initialState);
+  }
+
   showConfirmationModal(markAction: string, onSubmit: (id: number) => Observable<any>) {
     this.bsModalRef = this.modalService.show(ConfirmationModalComponent, {
       titleText: $localize`Mark OSD ${markAction}`,
index 831a022e1d7360aea285ed671b5e44bb0f267d00..9179a5e6d33764eb37d66cef4d167531262ce015 100644 (file)
@@ -99,6 +99,15 @@ describe('OsdService', () => {
     expect(req.request.body).toEqual({ flags: ['foo'] });
   });
 
+  it('should call updateIndividualFlags to update individual flags', () => {
+    const flags = { noin: true, noout: true };
+    const ids = [0, 1];
+    service.updateIndividualFlags(flags, ids).subscribe();
+    const req = httpTesting.expectOne('api/osd/flags/individual');
+    expect(req.request.method).toBe('PUT');
+    expect(req.request.body).toEqual({ flags: flags, ids: ids });
+  });
+
   it('should mark the OSD out', () => {
     service.markOut(1).subscribe();
     const req = httpTesting.expectOne('api/osd/1/mark');
index 2e698a220689f1c0e4ed9cfd3fee46c233d3c55a..9bff8083936e1fd2d6f07d89a05fa6e9a939a16f 100644 (file)
@@ -102,6 +102,10 @@ export class OsdService {
     return this.http.put(`${this.path}/flags`, { flags: flags });
   }
 
+  updateIndividualFlags(flags: { [flag: string]: boolean }, ids: number[]) {
+    return this.http.put(`${this.path}/flags/individual`, { flags: flags, ids: ids });
+  }
+
   markOut(id: number) {
     return this.http.put(`${this.path}/${id}/mark`, { action: 'out' });
   }
index 5f8d310d9c5248d2901cce12d4ca0cd3b678e1a8..3ce91b55ee273457b3c128f145e0e79fa4891f8d 100644 (file)
@@ -106,6 +106,7 @@ export class ActionLabelsI18n {
   UNPROTECT: string;
   UNSET: string;
   UPDATE: string;
+  FLAGS: string;
 
   constructor() {
     /* Create a new item */
@@ -149,6 +150,7 @@ export class ActionLabelsI18n {
     this.TRASH = $localize`Move to Trash`;
     this.UNPROTECT = $localize`Unprotect`;
     this.CHANGE = $localize`Change`;
+    this.FLAGS = $localize`Flags`;
 
     /* Prometheus wording */
     this.RECREATE = $localize`Recreate`;
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/flag.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/flag.ts
new file mode 100644 (file)
index 0000000..075decb
--- /dev/null
@@ -0,0 +1,8 @@
+export class Flag {
+  code: 'noout' | 'noin' | 'nodown' | 'noup';
+  name: string;
+  description: string;
+  value: boolean;
+  clusterWide: boolean;
+  indeterminate: boolean;
+}
index ffe1798762538752d57d39a870ace6bd1e77a8fd..bdbe2bfe465ae52c73db8864d08f01806c1a8093 100644 (file)
@@ -5149,12 +5149,143 @@ paths:
             schema:
               properties:
                 flags:
-                  type: string
+                  description: List of flags to set. The flags `recovery_deletes`,
+                    `sortbitwise` and `pglog_hardlimit` cannot be unset. Additionally
+                    `purged_snapshots` cannot even be set.
+                  items:
+                    type: string
+                  type: array
+              required:
+              - flags
+              type: object
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                properties:
+                  list_of_flags:
+                    description: ''
+                    items:
+                      type: string
+                    type: array
+                required:
+                - list_of_flags
+                type: object
+          description: Resource updated.
+        '202':
+          description: Operation is still executing. Please check the task queue.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Sets OSD flags for the entire cluster.
+      tags:
+      - OSD
+  /api/osd/flags/individual:
+    get:
+      parameters: []
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                properties:
+                  flags:
+                    description: List of active flags
+                    items:
+                      type: string
+                    type: array
+                  osd:
+                    description: OSD ID
+                    type: integer
+                required:
+                - osd
+                - flags
+                type: object
+          description: OK
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Displays individual OSD flags
+      tags:
+      - OSD
+    put:
+      description: "\n        Updates flags (`noout`, `noin`, `nodown`, `noup`) for\
+        \ an individual\n        subset of OSDs.\n        "
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                flags:
+                  description: Directory of flags to set or unset. The flags `noin`,
+                    `noout`, `noup` and `nodown` are going to be considered only.
+                  properties:
+                    nodown:
+                      description: Sets/unsets `nodown`
+                      type: boolean
+                    noin:
+                      description: Sets/unsets `noin`
+                      type: boolean
+                    noout:
+                      description: Sets/unsets `noout`
+                      type: boolean
+                    noup:
+                      description: Sets/unsets `noup`
+                      type: boolean
+                  type: object
+                ids:
+                  description: List of OSD ids the flags should be applied to.
+                  items:
+                    type: integer
+                  type: array
               required:
               - flags
+              - ids
               type: object
       responses:
         '200':
+          content:
+            application/json:
+              schema:
+                properties:
+                  added:
+                    description: List of added flags
+                    items:
+                      type: string
+                    type: array
+                  ids:
+                    description: List of updated OSDs
+                    items:
+                      type: integer
+                    type: array
+                  removed:
+                    description: List of removed flags
+                    items:
+                      type: string
+                    type: array
+                required:
+                - added
+                - removed
+                - ids
+                type: object
           description: Resource updated.
         '202':
           description: Operation is still executing. Please check the task queue.
@@ -5169,6 +5300,7 @@ paths:
             trace.
       security:
       - jwt: []
+      summary: Sets OSD flags for a subset of individual OSDs.
       tags:
       - OSD
   /api/osd/safe_to_delete: