- \(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 .+
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'])
"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)
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.
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
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';
ServiceDaemonListComponent,
TelemetryComponent,
PrometheusTabsComponent,
- ServiceFormComponent
+ ServiceFormComponent,
+ OsdFlagsIndivModalComponent
]
})
export class ClusterModule {}
--- /dev/null
+<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>
--- /dev/null
+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;
+ }
+});
--- /dev/null
+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();
+ }
+ );
+ }
+}
<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"
describe('getOsdList', () => {
let osds: any[];
+ let flagsSpy: jasmine.Spy;
const createOsd = (n: number) =>
<Record<string, any>>{
beforeEach(() => {
spyOn(osdService, 'getList').and.callFake(() => of(osds));
+ flagsSpy = spyOn(osdService, 'getFlags').and.callFake(() => of([]));
osds = [createOsd(1), createOsd(2), createOsd(3)];
component.getOsdList();
});
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', () => {
actions: [
'Create',
'Edit',
+ 'Flags',
'Scrub',
'Deep Scrub',
'Reweight',
actions: [
'Create',
'Edit',
+ 'Flags',
'Scrub',
'Deep Scrub',
'Reweight',
'update,delete': {
actions: [
'Edit',
+ 'Flags',
'Scrub',
'Deep Scrub',
'Reweight',
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: {
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';
safeToDestroyBodyTpl: TemplateRef<any>;
@ViewChild('deleteOsdExtraTpl')
deleteOsdExtraTpl: TemplateRef<any>;
+ @ViewChild('flagsTpl', { static: true })
+ flagsTpl: TemplateRef<any>;
permissions: Permissions;
tableActions: CdTableAction[];
selection = new CdTableSelection();
osds: any[] = [];
+ disabledFlags: string[] = [
+ 'sortbitwise',
+ 'purged_snapdirs',
+ 'recovery_deletes',
+ 'pglog_hardlimit'
+ ];
+ indivFlagNames: string[] = ['noup', 'nodown', 'noin', 'noout'];
orchStatus: OrchestratorStatus;
actionOrchFeatures = {
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',
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',
}
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;
});
});
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}`,
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');
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' });
}
UNPROTECT: string;
UNSET: string;
UPDATE: string;
+ FLAGS: string;
constructor() {
/* Create a new item */
this.TRASH = $localize`Move to Trash`;
this.UNPROTECT = $localize`Unprotect`;
this.CHANGE = $localize`Change`;
+ this.FLAGS = $localize`Flags`;
/* Prometheus wording */
this.RECREATE = $localize`Recreate`;
--- /dev/null
+export class Flag {
+ code: 'noout' | 'noin' | 'nodown' | 'noup';
+ name: string;
+ description: string;
+ value: boolean;
+ clusterWide: boolean;
+ indeterminate: boolean;
+}
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.
trace.
security:
- jwt: []
+ summary: Sets OSD flags for a subset of individual OSDs.
tags:
- OSD
/api/osd/safe_to_delete: