import json
import logging
from functools import wraps
-from typing import List
+from typing import List, Optional
from smb.enums import Intent
from smb.proto import Simplified
from smb.resources import Cluster, JoinAuth, Share, UsersAndGroups
from dashboard.controllers._docs import EndpointDoc
-from dashboard.controllers._permissions import CreatePermission, DeletePermission
+from dashboard.controllers._permissions import CreatePermission, DeletePermission, UpdatePermission
from dashboard.exceptions import DashboardException
from .. import mgr
"provider": (str, "Provider of the CephFS share, e.g., 'samba-vfs'"),
"subvolumegroup": (str, "Subvolume Group in CephFS file system"),
"subvolume": (str, "Subvolume within the CephFS file system"),
- }, "Configuration for the CephFS share")
+ }, "Configuration for the CephFS share"),
+ "read_iops_limit": (int, "QoS: max read IOPS (0=disabled)"),
+ "write_iops_limit": (int, "QoS: max write IOPS (0=disabled)"),
+ "read_bw_limit": (int, "QoS: max read bandwidth B/s (0=disabled)"),
+ "write_bw_limit": (int, "QoS: max write bandwidth B/s (0=disabled)"),
+ "read_delay_max": (int, "QoS: max read delay in seconds (0-300)"),
+ "write_delay_max": (int, "QoS: max write delay in seconds (0-300)"),
}
JOIN_AUTH_SCHEMA = {
return mgr.remote('smb', 'show', [f'{self._resource}.{cluster_id}.{share_id}'])
@raise_on_failure
- @DeletePermission
+ @UpdatePermission
+ @Endpoint(method='PUT', path='/qos')
+ @EndpointDoc(
+ "Update SMB share QoS rate limiting",
+ parameters={
+ 'cluster_id': (str, 'Unique identifier for the cluster'),
+ 'share_id': (str, 'Unique identifier for the share'),
+ 'read_iops_limit': (int, 'Max read IOPS (0=disabled, 0-1000000)'),
+ 'write_iops_limit': (int, 'Max write IOPS (0=disabled, 0-1000000)'),
+ 'read_bw_limit': (int, 'Max read bandwidth B/s (0=disabled)'),
+ 'write_bw_limit': (int, 'Max write bandwidth B/s (0=disabled)'),
+ 'read_delay_max': (int, 'Max read delay in seconds (0-300)'),
+ 'write_delay_max': (int, 'Max write delay in seconds (0-300)'),
+ })
+ def update_qos(
+ self,
+ cluster_id: str,
+ share_id: str,
+ read_iops_limit: Optional[int] = None,
+ write_iops_limit: Optional[int] = None,
+ read_bw_limit: Optional[int] = None,
+ write_bw_limit: Optional[int] = None,
+ read_delay_max: Optional[int] = None,
+ write_delay_max: Optional[int] = None,
+ ):
+ """
+ Update QoS rate limit parameters for an SMB share.
+ Omitted parameters are left unchanged. Use 0 to disable a limit.
+ """
+ return mgr.remote(
+ 'smb',
+ 'share_update_qos',
+ cluster_id,
+ share_id,
+ read_iops_limit,
+ write_iops_limit,
+ read_bw_limit,
+ write_bw_limit,
+ read_delay_max,
+ write_delay_max
+ ).to_simplified()
+
@EndpointDoc("Remove an smb share",
parameters={
'cluster_id': (str, 'Unique identifier for the cluster'),
<cd-help-text>If selected no clients are permitted to write to the share.</cd-help-text>
</cds-checkbox>
</div>
+
+ <!-- QoS rate limiting -->
+ <div class="form-header"
+ i18n>Rate limiting</div>
+ <div class="form-item form-item-append"
+ cdsRow>
+ <div cdsCol>
+ <cds-number formControlName="read_iops_limit"
+ cdValidate
+ #readIopsRef="cdValidate"
+ label="Read IOPS limit"
+ i18n-label
+ [min]="0"
+ [max]="1000000"
+ helperText="Max read operations per second (0 = disabled)"
+ i18n-helperText
+ [invalid]="readIopsRef.isInvalid"
+ invalidText="Value must be between 0 and 1,000,000"
+ i18n-invalidText>
+ </cds-number>
+ </div>
+ <div cdsCol>
+ <cds-number formControlName="write_iops_limit"
+ cdValidate
+ #writeIopsRef="cdValidate"
+ label="Write IOPS limit"
+ i18n-label
+ [min]="0"
+ [max]="1000000"
+ helperText="Max write operations per second (0 = disabled)"
+ i18n-helperText
+ [invalid]="writeIopsRef.isInvalid"
+ invalidText="Value must be between 0 and 1,000,000"
+ i18n-invalidText>
+ </cds-number>
+ </div>
+ </div>
+ <div class="form-item form-item-append"
+ cdsRow>
+ <div cdsCol>
+ <cd-number-with-unit
+ [form]="smbShareForm"
+ valueControlName="read_bw_limit"
+ unitControlName="read_bw_limit_unit"
+ label="Read bandwidth limit"
+ i18n-label
+ helperText="Max read bandwidth (0 = disabled)"
+ i18n-helperText
+ [units]="qosBwUnits"
+ [min]="0"
+ [max]="readBwMax"
+ invalidText="Value must be between 0 and {{ readBwMax | number }}"
+ i18n-invalidText
+ ></cd-number-with-unit>
+ </div>
+ <div cdsCol>
+ <cd-number-with-unit
+ [form]="smbShareForm"
+ valueControlName="write_bw_limit"
+ unitControlName="write_bw_limit_unit"
+ label="Write bandwidth limit"
+ i18n-label
+ helperText="Max write bandwidth (0 = disabled)"
+ i18n-helperText
+ [units]="qosBwUnits"
+ [min]="0"
+ [max]="writeBwMax"
+ invalidText="Value must be between 0 and {{ writeBwMax | number }}"
+ i18n-invalidText
+ ></cd-number-with-unit>
+ </div>
+ </div>
+ <div class="form-item form-item-append"
+ cdsRow>
+ <div cdsCol>
+ <cds-number formControlName="read_delay_max"
+ cdValidate
+ #readDelayRef="cdValidate"
+ label="Read delay max (s)"
+ i18n-label
+ [min]="0"
+ [max]="300"
+ helperText="Max allowed delay for read operations in seconds"
+ i18n-helperText
+ [invalid]="readDelayRef.isInvalid"
+ invalidText="Value must be between 0 and 300"
+ i18n-invalidText>
+ </cds-number>
+ </div>
+ <div cdsCol>
+ <cds-number formControlName="write_delay_max"
+ cdValidate
+ #writeDelayRef="cdValidate"
+ label="Write delay max (s)"
+ i18n-label
+ [min]="0"
+ [max]="300"
+ helperText="Max allowed delay for write operations in seconds"
+ i18n-helperText
+ [invalid]="writeDelayRef.isInvalid"
+ invalidText="Value must be between 0 and 300"
+ i18n-invalidText>
+ </cds-number>
+ </div>
+ </div>
+
<cd-form-button-panel
(submitActionEvent)="submitAction()"
[form]="smbShareForm"
ComboBoxModule,
GridModule,
InputModule,
+ NumberModule,
SelectModule
} from 'carbon-components-angular';
import { SmbService } from '~/app/shared/api/smb.service';
ToastrModule.forRoot(),
GridModule,
InputModule,
+ NumberModule,
SelectModule,
ComboBoxModule,
CheckboxModule
prefixedPath: '/volumes/fs1/group1/subvol1',
inputPath: '/',
browseable: true,
- readonly: false
+ readonly: false,
+ read_iops_limit: 0,
+ write_iops_limit: 0,
+ read_bw_limit: 0,
+ read_bw_limit_unit: 'MiB',
+ write_bw_limit: 0,
+ write_bw_limit_unit: 'MiB',
+ read_delay_max: 30,
+ write_delay_max: 30
});
component.submitAction();
expect(component).toBeTruthy();
});
+
+ describe('QoS', () => {
+ it('should have QoS form controls with default values', () => {
+ component.ngOnInit();
+ expect(component.smbShareForm.get('read_iops_limit')).toBeTruthy();
+ expect(component.smbShareForm.get('write_iops_limit')).toBeTruthy();
+ expect(component.smbShareForm.get('read_bw_limit')).toBeTruthy();
+ expect(component.smbShareForm.get('read_bw_limit_unit')).toBeTruthy();
+ expect(component.smbShareForm.get('write_bw_limit')).toBeTruthy();
+ expect(component.smbShareForm.get('write_bw_limit_unit')).toBeTruthy();
+ expect(component.smbShareForm.get('read_delay_max')).toBeTruthy();
+ expect(component.smbShareForm.get('write_delay_max')).toBeTruthy();
+ expect(component.smbShareForm.get('read_iops_limit').value).toBe(0);
+ expect(component.smbShareForm.get('write_delay_max').value).toBe(30);
+ });
+
+ it('should include QoS fields in buildRequest when set', () => {
+ component.clusterId = 'cluster1';
+ component.smbShareForm.patchValue({
+ share_id: 'share1',
+ name: 'My Share',
+ volume: 'fs1',
+ inputPath: '/',
+ read_iops_limit: 1000,
+ write_iops_limit: 500,
+ read_bw_limit: 100,
+ read_bw_limit_unit: 'MiB',
+ write_delay_max: 60
+ });
+ const request = component.buildRequest();
+ expect(request.share_resource.cephfs.qos.read_iops_limit).toBe(1000);
+ expect(request.share_resource.cephfs.qos.write_iops_limit).toBe(500);
+ expect(request.share_resource.cephfs.qos.read_bw_limit).toBe(100 * 1024 * 1024);
+ expect(request.share_resource.cephfs.qos.write_delay_max).toBe(60);
+ });
+ });
});
import { SmbService } from '~/app/shared/api/smb.service';
import { NfsService } from '~/app/shared/api/nfs.service';
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-group.service';
import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service';
import { CLUSTER_PATH } from '../smb-cluster-list/smb-cluster-list.component';
import { SHARE_PATH } from '../smb-share-list/smb-share-list.component';
+const QOS_IOPS_MAX = 1_000_000;
+const QOS_BW_MAX_BYTES = 2 ** 40;
+const UNIT_TO_BYTES: Record<string, number> = {
+ KiB: 2 ** 10,
+ MiB: 2 ** 20,
+ GiB: 2 ** 30,
+ TiB: 2 ** 40
+};
+const QOS_DELAY_MAX = 300;
+const QOS_DELAY_DEFAULT = 30;
+const QOS_BW_UNITS = ['KiB', 'MiB', 'GiB', 'TiB'];
+
+function getBwMaxForUnit(unit: string): number {
+ return Math.floor(QOS_BW_MAX_BYTES / UNIT_TO_BYTES[unit]);
+}
+
@Component({
selector: 'cd-smb-share-form',
templateUrl: './smb-share-form.component.html',
isEdit = false;
share_id: string;
shareResponse: SMBShare;
+ qosBwUnits = QOS_BW_UNITS;
+ readBwMax = getBwMaxForUnit(QOS_BW_UNITS[1]);
+ writeBwMax = getBwMaxForUnit(QOS_BW_UNITS[1]);
constructor(
private formBuilder: CdFormBuilder,
private subvolService: CephfsSubvolumeService,
private taskWrapperService: TaskWrapperService,
private router: Router,
- private route: ActivatedRoute
+ private route: ActivatedRoute,
+ private formatter: FormatterService,
+ private dimlessBinaryPipe: DimlessBinaryPipe
) {
super();
this.resource = $localize`Share`;
if (this.isEdit) {
this.smbService.getShare(this.clusterId, this.share_id).subscribe((resp: SMBShare) => {
this.shareResponse = resp;
- this.smbShareForm.get('share_id').setValue(this.shareResponse.share_id);
+ const cephfs = this.shareResponse?.cephfs;
+ const qos = cephfs?.qos;
+
+ this.smbShareForm.patchValue({
+ share_id: this.shareResponse.share_id,
+ name: this.shareResponse.name,
+ volume: cephfs?.volume,
+ subvolume_group: cephfs?.subvolumegroup,
+ subvolume: cephfs?.subvolume,
+ inputPath: cephfs?.path,
+ readonly: this.shareResponse.readonly ?? false,
+ browseable: this.shareResponse.browseable ?? true,
+ read_iops_limit: qos?.read_iops_limit,
+ write_iops_limit: qos?.write_iops_limit,
+ read_delay_max: qos?.read_delay_max,
+ write_delay_max: qos?.write_delay_max
+ });
this.smbShareForm.get('share_id').disable();
- this.smbShareForm.get('name').setValue(this.shareResponse.name);
this.smbShareForm.get('name').disable();
- this.smbShareForm.get('volume').setValue(this.shareResponse?.cephfs?.volume);
- this.smbShareForm
- .get('subvolume_group')
- .setValue(this.shareResponse?.cephfs?.subvolumegroup);
- this.smbShareForm.get('subvolume').setValue(this.shareResponse?.cephfs?.subvolume);
- this.smbShareForm.get('inputPath').setValue(this.shareResponse?.cephfs?.path);
- if (this.shareResponse.readonly) {
- this.smbShareForm.get('readonly').setValue(this.shareResponse.readonly);
- }
- if (this.shareResponse.browseable) {
- this.smbShareForm.get('browseable').setValue(this.shareResponse.browseable);
- }
+ this.setBwLimitFromBytes('read_bw_limit', qos?.read_bw_limit);
+ this.setBwLimitFromBytes('write_bw_limit', qos?.write_bw_limit);
- this.getSubVolGrp(this.shareResponse?.cephfs?.volume);
+ this.getSubVolGrp(cephfs?.volume);
});
}
+ this.smbShareForm.get('read_bw_limit_unit').valueChanges.subscribe((unit: string) => {
+ this.readBwMax = getBwMaxForUnit(unit);
+ const ctrl = this.smbShareForm.get('read_bw_limit');
+ ctrl.setValidators([Validators.min(0), Validators.max(this.readBwMax)]);
+ ctrl.updateValueAndValidity();
+ });
+ this.smbShareForm.get('write_bw_limit_unit').valueChanges.subscribe((unit: string) => {
+ this.writeBwMax = getBwMaxForUnit(unit);
+ const ctrl = this.smbShareForm.get('write_bw_limit');
+ ctrl.setValidators([Validators.min(0), Validators.max(this.writeBwMax)]);
+ ctrl.updateValueAndValidity();
+ });
this.smbShareForm.get('share_id')?.valueChanges.subscribe((value) => {
const shareName = this.smbShareForm.get('name');
if (shareName && !shareName.dirty) {
this.loadingReady();
}
+ private setBwLimitFromBytes(prefix: string, bytes: number) {
+ const parts = this.dimlessBinaryPipe.transform(bytes).split(' ');
+ this.smbShareForm.get(prefix).setValue(parts[0] || 0);
+ this.smbShareForm.get(prefix + '_unit').setValue(parts[1] || QOS_BW_UNITS[1]);
+ }
+
createForm() {
this.smbShareForm = this.formBuilder.group({
share_id: new FormControl('', {
validators: [Validators.required]
}),
browseable: new FormControl(true),
- readonly: new FormControl(false)
+ readonly: new FormControl(false),
+ read_iops_limit: new FormControl(0, [Validators.min(0), Validators.max(QOS_IOPS_MAX)]),
+ write_iops_limit: new FormControl(0, [Validators.min(0), Validators.max(QOS_IOPS_MAX)]),
+ read_bw_limit: new FormControl(0, [Validators.min(0), Validators.max(this.readBwMax)]),
+ read_bw_limit_unit: new FormControl(QOS_BW_UNITS[1]),
+ write_bw_limit: new FormControl(0, [Validators.min(0), Validators.max(this.writeBwMax)]),
+ write_bw_limit_unit: new FormControl(QOS_BW_UNITS[1]),
+ read_delay_max: new FormControl(QOS_DELAY_DEFAULT, [
+ Validators.min(0),
+ Validators.max(QOS_DELAY_MAX)
+ ]),
+ write_delay_max: new FormControl(QOS_DELAY_DEFAULT, [
+ Validators.min(0),
+ Validators.max(QOS_DELAY_MAX)
+ ])
});
}
path: correctedPath,
subvolumegroup: rawFormValue.subvolume_group,
subvolume: rawFormValue.subvolume,
- provider: PROVIDER
+ provider: PROVIDER,
+ qos: {
+ read_iops_limit: rawFormValue.read_iops_limit,
+ write_iops_limit: rawFormValue.write_iops_limit,
+ read_bw_limit: this.formatter.toBytes(
+ String(this.smbShareForm.get('read_bw_limit').value) +
+ ' ' +
+ this.smbShareForm.get('read_bw_limit_unit').value
+ ),
+ write_bw_limit: this.formatter.toBytes(
+ String(this.smbShareForm.get('write_bw_limit').value) +
+ ' ' +
+ this.smbShareForm.get('write_bw_limit_unit').value
+ ),
+ read_delay_max: rawFormValue.read_delay_max,
+ write_delay_max: rawFormValue.write_delay_max
+ }
},
browseable: rawFormValue.browseable,
readonly: rawFormValue.readonly
</div>
</cd-table>
</ng-container>
+
+<ng-template #iopsLimitTpl
+ let-value="data.value">
+ R: {{ value?.read_iops_limit ? (value.read_iops_limit | number) : '-' }}
+ / W: {{ value?.write_iops_limit ? (value.write_iops_limit | number) : '-' }}
+</ng-template>
+
+<ng-template #bwLimitTpl
+ let-value="data.value">
+ R: {{ value?.read_bw_limit ? (value.read_bw_limit | dimlessBinary) : '-' }}
+ / W: {{ value?.write_bw_limit ? (value.write_bw_limit | dimlessBinary) : '-' }}
+</ng-template>
+
+<ng-template #delayMaxTpl
+ let-value="data.value">
+ R: {{ value?.read_delay_max ? value.read_delay_max + 's' : '-' }}
+ / W: {{ value?.write_delay_max ? value.write_delay_max + 's' : '-' }}
+</ng-template>
-import { Component, Input, OnInit, ViewChild } from '@angular/core';
+import { Component, Input, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Observable, BehaviorSubject, of } from 'rxjs';
import { switchMap, catchError } from 'rxjs/operators';
import { TableComponent } from '~/app/shared/datatable/table/table.component';
clusterId: string;
@ViewChild('table', { static: true })
table: TableComponent;
+ @ViewChild('iopsLimitTpl', { static: true })
+ iopsLimitTpl: TemplateRef<any>;
+ @ViewChild('bwLimitTpl', { static: true })
+ bwLimitTpl: TemplateRef<any>;
+ @ViewChild('delayMaxTpl', { static: true })
+ delayMaxTpl: TemplateRef<any>;
columns: CdTableColumn[];
permission: Permission;
selection = new CdTableSelection();
name: $localize`Provider`,
prop: 'cephfs.provider',
flexGrow: 2
+ },
+ {
+ name: $localize`IOPS Limit`,
+ prop: 'cephfs.qos',
+ cellTemplate: this.iopsLimitTpl,
+ flexGrow: 2
+ },
+ {
+ name: $localize`Bandwidth Limit`,
+ prop: 'cephfs.qos',
+ cellTemplate: this.bwLimitTpl,
+ flexGrow: 2
+ },
+ {
+ name: $localize`Delay Max`,
+ prop: 'cephfs.qos',
+ cellTemplate: this.delayMaxTpl,
+ flexGrow: 2
}
];
this.tableActions = [
subvolumegroup?: string;
subvolume?: string;
provider?: string;
+ qos?: SMBShareQoS;
}
interface SMBShareLoginControl {
label: 'label'
};
+export interface SMBShareQoS {
+ read_iops_limit?: number;
+ write_iops_limit?: number;
+ read_bw_limit?: number;
+ write_bw_limit?: number;
+ read_delay_max?: number;
+ write_delay_max?: number;
+}
+
export interface SMBShare {
resource_type: string;
cluster_id: string;
login_control?: SMBShareLoginControl;
}
-interface SMBCephfs {
- volume: string;
- path: string;
- subvolumegroup?: string;
- subvolume?: string;
- provider?: string;
-}
-
-interface SMBShareLoginControl {
- name: string;
- access: 'read' | 'read-write' | 'none' | 'admin';
- category?: typeof USER | 'group';
-}
-
export interface SMBJoinAuth {
resource_type: string;
auth_id: string;
import { TearsheetStepComponent } from './tearsheet-step/tearsheet-step.component';
import { PageHeaderComponent } from './page-header/page-header.component';
import { SidebarLayoutComponent } from './sidebar-layout/sidebar-layout.component';
+import { NumberWithUnitComponent } from './number-with-unit/number-with-unit.component';
@NgModule({
imports: [
TearsheetComponent,
TearsheetStepComponent,
PageHeaderComponent,
- SidebarLayoutComponent
+ SidebarLayoutComponent,
+ NumberWithUnitComponent
],
providers: [provideCharts(withDefaultRegisterables())],
exports: [
TearsheetComponent,
TearsheetStepComponent,
PageHeaderComponent,
- SidebarLayoutComponent
+ SidebarLayoutComponent,
+ NumberWithUnitComponent
]
})
export class ComponentsModule {
--- /dev/null
+<div
+ cdsRow
+ [formGroup]="formGroup"
+>
+ <div cdsCol>
+ <cds-number
+ [label]="label"
+ [helperText]="helperText"
+ cdValidate
+ #valueRef="cdValidate"
+ [formControlName]="valueControlName"
+ [min]="min"
+ [max]="max"
+ [invalid]="valueRef.isInvalid"
+ [invalidText]="invalidText"
+ >
+ </cds-number>
+ </div>
+ <div cdsCol>
+ <cds-select
+ [formControlName]="unitControlName"
+ [label]="unitLabel"
+ [id]="unitControlName"
+ >
+ @for (unit of units; track unit) {
+ <option [value]="unit">{{ unit }}</option>
+ }
+ </cds-select>
+ </div>
+</div>
--- /dev/null
+import { Component, Input, Optional, TemplateRef } from '@angular/core';
+import { ControlContainer, FormGroup } from '@angular/forms';
+
+@Component({
+ selector: 'cd-number-with-unit',
+ templateUrl: './number-with-unit.component.html',
+ styleUrls: ['./number-with-unit.component.scss'],
+ standalone: false
+})
+export class NumberWithUnitComponent {
+ @Input()
+ form: FormGroup;
+
+ @Input()
+ valueControlName: string;
+
+ /** Resolved form: parent form from ControlContainer when inside a form, otherwise @Input() form. */
+ get formGroup(): FormGroup {
+ const parent = this.controlContainer?.control as FormGroup | undefined;
+ return parent ?? this.form;
+ }
+
+ constructor(@Optional() private controlContainer: ControlContainer) {}
+
+ @Input()
+ unitControlName: string;
+
+ @Input()
+ label: string;
+
+ @Input()
+ helperText: string;
+
+ @Input()
+ units: string[] = ['KiB', 'MiB', 'GiB', 'TiB'];
+
+ @Input()
+ min: number = 0;
+
+ @Input()
+ max: number | undefined;
+
+ @Input()
+ unitLabel: string = $localize`Unit`;
+
+ @Input()
+ invalidText: string | TemplateRef<unknown> = $localize`The value should be greater or equal to 0`;
+
+ get valueControl() {
+ return this.formGroup?.get(this.valueControlName);
+ }
+
+ get unitControl() {
+ return this.formGroup?.get(this.unitControlName);
+ }
+
+ get invalidTextIsTemplate(): boolean {
+ return this.invalidText instanceof TemplateRef;
+ }
+}
name:
description: Name of the share
type: string
+ read_bw_limit:
+ description: 'QoS: max read bandwidth B/s (0=disabled)'
+ type: integer
+ read_delay_max:
+ description: 'QoS: max read delay in seconds (0-300)'
+ type: integer
+ read_iops_limit:
+ description: 'QoS: max read IOPS (0=disabled)'
+ type: integer
readonly:
description: Indicates if the share is read-only
type: boolean
share_id:
description: Unique identifier for the share
type: string
+ write_bw_limit:
+ description: 'QoS: max write bandwidth B/s (0=disabled)'
+ type: integer
+ write_delay_max:
+ description: 'QoS: max write delay in seconds (0-300)'
+ type: integer
+ write_iops_limit:
+ description: 'QoS: max write IOPS (0=disabled)'
+ type: integer
required: &id158
- resource_type
- cluster_id
- readonly
- browseable
- cephfs
+ - read_iops_limit
+ - write_iops_limit
+ - read_bw_limit
+ - write_bw_limit
+ - read_delay_max
+ - write_delay_max
type: object
application/vnd.ceph.api.v1.0+json:
schema:
name:
description: Name of the share
type: string
+ read_bw_limit:
+ description: 'QoS: max read bandwidth B/s (0=disabled)'
+ type: integer
+ read_delay_max:
+ description: 'QoS: max read delay in seconds (0-300)'
+ type: integer
+ read_iops_limit:
+ description: 'QoS: max read IOPS (0=disabled)'
+ type: integer
readonly:
description: Indicates if the share is read-only
type: boolean
share_id:
description: Unique identifier for the share
type: string
+ write_bw_limit:
+ description: 'QoS: max write bandwidth B/s (0=disabled)'
+ type: integer
+ write_delay_max:
+ description: 'QoS: max write delay in seconds (0-300)'
+ type: integer
+ write_iops_limit:
+ description: 'QoS: max write IOPS (0=disabled)'
+ type: integer
required: *id158
type: object
description: OK
name:
description: Name of the share
type: string
+ read_bw_limit:
+ description: 'QoS: max read bandwidth B/s (0=disabled)'
+ type: integer
+ read_delay_max:
+ description: 'QoS: max read delay in seconds (0-300)'
+ type: integer
+ read_iops_limit:
+ description: 'QoS: max read IOPS (0=disabled)'
+ type: integer
readonly:
description: Indicates if the share is read-only
type: boolean
share_id:
description: Unique identifier for the share
type: string
+ write_bw_limit:
+ description: 'QoS: max write bandwidth B/s (0=disabled)'
+ type: integer
+ write_delay_max:
+ description: 'QoS: max write delay in seconds (0-300)'
+ type: integer
+ write_iops_limit:
+ description: 'QoS: max write IOPS (0=disabled)'
+ type: integer
required: &id160
- resource_type
- cluster_id
- readonly
- browseable
- cephfs
+ - read_iops_limit
+ - write_iops_limit
+ - read_bw_limit
+ - write_bw_limit
+ - read_delay_max
+ - write_delay_max
type: object
state:
description: The current state of the resource, e.g.,
name:
description: Name of the share
type: string
+ read_bw_limit:
+ description: 'QoS: max read bandwidth B/s (0=disabled)'
+ type: integer
+ read_delay_max:
+ description: 'QoS: max read delay in seconds (0-300)'
+ type: integer
+ read_iops_limit:
+ description: 'QoS: max read IOPS (0=disabled)'
+ type: integer
readonly:
description: Indicates if the share is read-only
type: boolean
share_id:
description: Unique identifier for the share
type: string
+ write_bw_limit:
+ description: 'QoS: max write bandwidth B/s (0=disabled)'
+ type: integer
+ write_delay_max:
+ description: 'QoS: max write delay in seconds (0-300)'
+ type: integer
+ write_iops_limit:
+ description: 'QoS: max write IOPS (0=disabled)'
+ type: integer
required: *id160
type: object
state:
summary: Create smb share
tags:
- SMB
+ /api/smb/share/qos:
+ put:
+ description: "\n Update QoS rate limit parameters for an SMB share.\n\
+ \ Omitted parameters are left unchanged. Use 0 to disable a limit.\n\
+ \ "
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ cluster_id:
+ description: Unique identifier for the cluster
+ type: string
+ read_bw_limit:
+ description: Max read bandwidth B/s (0=disabled)
+ type: integer
+ read_delay_max:
+ description: Max read delay in seconds (0-300)
+ type: integer
+ read_iops_limit:
+ description: Max read IOPS (0=disabled, 0-1000000)
+ type: integer
+ share_id:
+ description: Unique identifier for the share
+ type: string
+ write_bw_limit:
+ description: Max write bandwidth B/s (0=disabled)
+ type: integer
+ write_delay_max:
+ description: Max write delay in seconds (0-300)
+ type: integer
+ write_iops_limit:
+ description: Max write IOPS (0=disabled, 0-1000000)
+ type: integer
+ required:
+ - cluster_id
+ - share_id
+ type: object
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ type: object
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/json:
+ schema:
+ type: object
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ type: object
+ 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: Update SMB share QoS rate limiting
+ tags:
+ - SMB
/api/smb/share/{cluster_id}/{share_id}:
delete:
description: "\n Remove an smb share from a given cluster\n\n \
name:
description: Name of the share
type: string
+ read_bw_limit:
+ description: 'QoS: max read bandwidth B/s (0=disabled)'
+ type: integer
+ read_delay_max:
+ description: 'QoS: max read delay in seconds (0-300)'
+ type: integer
+ read_iops_limit:
+ description: 'QoS: max read IOPS (0=disabled)'
+ type: integer
readonly:
description: Indicates if the share is read-only
type: boolean
share_id:
description: Unique identifier for the share
type: string
+ write_bw_limit:
+ description: 'QoS: max write bandwidth B/s (0=disabled)'
+ type: integer
+ write_delay_max:
+ description: 'QoS: max write delay in seconds (0-300)'
+ type: integer
+ write_iops_limit:
+ description: 'QoS: max write IOPS (0=disabled)'
+ type: integer
required: &id164
- resource_type
- cluster_id
- readonly
- browseable
- cephfs
+ - read_iops_limit
+ - write_iops_limit
+ - read_bw_limit
+ - write_bw_limit
+ - read_delay_max
+ - write_delay_max
type: object
application/vnd.ceph.api.v1.0+json:
schema:
name:
description: Name of the share
type: string
+ read_bw_limit:
+ description: 'QoS: max read bandwidth B/s (0=disabled)'
+ type: integer
+ read_delay_max:
+ description: 'QoS: max read delay in seconds (0-300)'
+ type: integer
+ read_iops_limit:
+ description: 'QoS: max read IOPS (0=disabled)'
+ type: integer
readonly:
description: Indicates if the share is read-only
type: boolean
share_id:
description: Unique identifier for the share
type: string
+ write_bw_limit:
+ description: 'QoS: max write bandwidth B/s (0=disabled)'
+ type: integer
+ write_delay_max:
+ description: 'QoS: max write delay in seconds (0-300)'
+ type: integer
+ write_iops_limit:
+ description: 'QoS: max write IOPS (0=disabled)'
+ type: integer
required: *id164
type: object
description: OK
self.assertStatus(204)
mgr.remote.assert_called_once_with('smb', 'apply_resources', json.dumps(_res_simplified))
+ def test_get_share_with_qos(self):
+ import copy
+ share_with_qos = copy.deepcopy(self._shares['resources'][0])
+ share_with_qos['cephfs']['qos'] = {
+ 'read_iops_limit': 1000,
+ 'write_iops_limit': 500,
+ 'read_bw_limit': 100000,
+ 'write_bw_limit': 100000,
+ 'read_delay_max': 30,
+ 'write_delay_max': 60,
+ }
+ mgr.remote = Mock(return_value=share_with_qos)
+
+ self._get(f'{self._endpoint}/smbCluster1/share1')
+ self.assertStatus(200)
+ self.assertJsonBody(share_with_qos)
+ mgr.remote.assert_called_once_with(
+ 'smb', 'show', ['ceph.smb.share.smbCluster1.share1']
+ )
+
+ def test_update_qos(self):
+ cluster_id = 'smbCluster1'
+ share_id = 'share1'
+
+ mock_result = Mock()
+ mock_result.to_simplified.return_value = {
+ 'resource_type': 'ceph.smb.share',
+ 'cluster_id': cluster_id,
+ 'share_id': share_id,
+ }
+ mgr.remote = Mock(return_value=mock_result)
+
+ qos_body = {
+ 'cluster_id': cluster_id,
+ 'share_id': share_id,
+ 'read_iops_limit': 1000,
+ 'write_iops_limit': 500,
+ }
+ self._put(f'{self._endpoint}/qos', qos_body)
+ self.assertStatus(200)
+
+ mgr.remote.assert_called_once_with(
+ 'smb', 'share_update_qos',
+ cluster_id, share_id,
+ 1000, 500, None, None, None, None
+ )
+
class SMBJoinAuthTest(ControllerTestCase):
_endpoint = '/api/smb/joinauth'