</label>
<div class="cd-col-form-input">
<input *ngIf="edit"
- name="pool"
class="form-control"
type="text"
formControlName="pool">
<select *ngIf="!edit"
id="pool"
- name="pool"
class="form-select"
formControlName="pool">
<option *ngIf="rbdPools === null"
i18n>This field is required.</span>
</div>
</div>
- <!-- Image Name -->
- <div class="form-group row">
+ <!-- Namespace Count -->
+ <div *ngIf="!edit"
+ class="form-group row">
<label class="cd-col-form-label"
- for="image">
+ for="nsCount">
<span [ngClass]="{'required': !edit}"
- i18n>Image Name</span>
+ i18n>Namespace Count</span>
</label>
<div class="cd-col-form-input">
- <input name="image"
- class="form-control"
- type="text"
- formControlName="image">
- <span class="invalid-feedback"
- *ngIf="nsForm.showError('image', formDir, 'required')">
- <ng-container i18n>This field is required.</ng-container>
- </span>
- <span class="invalid-feedback"
- *ngIf="nsForm.showError('image', formDir, 'pattern')">
- <ng-container i18n>'/' and '@' are not allowed.</ng-container>
- </span>
+ <cds-number
+ formControlName="nsCount"
+ helperText="The number of namespaces to create"
+ i18n-helperText
+ [min]="MIN_NAMESPACE_CREATE"
+ [max]="MAX_NAMESPACE_CREATE"
+ [invalid]="nsForm.showError('nsCount', formDir, 'max') || nsForm.showError('nsCount', formDir, 'min') || nsForm.showError('nsCount', formDir, 'required')"
+ [invalidText]="nsForm.get('nsCount').hasError('required') ? requiredInvalidText: nsCountInvalidText"
+ size="sm"></cds-number>
</div>
</div>
<!-- Image Size -->
<input id="size"
class="form-control"
type="text"
- name="image_size"
formControlName="image_size">
<select id="unit"
- name="unit"
class="form-input form-select"
formControlName="unit">
<option *ngFor="let u of units"
import { SharedModule } from '~/app/shared/shared.module';
import { NvmeofNamespacesFormComponent } from './nvmeof-namespaces-form.component';
-import { FormHelper } from '~/testing/unit-test-helper';
+import { FormHelper, Mocks } from '~/testing/unit-test-helper';
import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { of } from 'rxjs';
+import { PoolService } from '~/app/shared/api/pool.service';
+import { NumberModule } from 'carbon-components-angular';
+
+const mockPools = [
+ Mocks.getPool('pool-1', 1, ['cephfs']),
+ Mocks.getPool('rbd', 2),
+ Mocks.getPool('pool-2', 3)
+];
+class MockPoolService {
+ getList() {
+ return of(mockPools);
+ }
+}
describe('NvmeofNamespacesFormComponent', () => {
let component: NvmeofNamespacesFormComponent;
let nvmeofService: NvmeofService;
let form: CdFormGroup;
let formHelper: FormHelper;
- const mockTimestamp = 1720693470789;
+ const mockRandomString = 1720693470789;
const mockSubsystemNQN = 'nqn.2021-11.com.example:subsystem';
+ const mockGWgroup = 'default';
beforeEach(async () => {
- spyOn(Date, 'now').and.returnValue(mockTimestamp);
await TestBed.configureTestingModule({
declarations: [NvmeofNamespacesFormComponent],
- providers: [NgbActiveModal],
+ providers: [NgbActiveModal, { provide: PoolService, useClass: MockPoolService }],
imports: [
HttpClientTestingModule,
NgbTypeaheadModule,
ReactiveFormsModule,
RouterTestingModule,
SharedModule,
+ NumberModule,
ToastrModule.forRoot()
]
}).compileComponents();
-
fixture = TestBed.createComponent(NvmeofNamespacesFormComponent);
component = fixture.componentInstance;
component.ngOnInit();
describe('should test form', () => {
beforeEach(() => {
component.subsystemNQN = mockSubsystemNQN;
+ component.group = mockGWgroup;
nvmeofService = TestBed.inject(NvmeofService);
spyOn(nvmeofService, 'createNamespace').and.stub();
+ spyOn(component, 'randomString').and.returnValue(mockRandomString);
});
-
- it('should be creating request correctly', () => {
- const image = 'nvme_ns_image:' + mockTimestamp;
+ it('should create 5 namespaces correctly', () => {
component.onSubmit();
+ expect(nvmeofService.createNamespace).toHaveBeenCalledTimes(5);
expect(nvmeofService.createNamespace).toHaveBeenCalledWith(mockSubsystemNQN, {
- rbd_image_name: image,
- rbd_pool: null,
+ gw_group: mockGWgroup,
+ rbd_image_name: `nvme_rbd_default_${mockRandomString}`,
+ rbd_pool: 'rbd',
rbd_image_size: 1073741824
});
});
-
- it('should give error on invalid image name', () => {
- formHelper.setValue('image', '/ghfhdlk;kd;@');
- component.onSubmit();
- formHelper.expectError('image', 'pattern');
- });
-
it('should give error on invalid image size', () => {
formHelper.setValue('image_size', -56);
component.onSubmit();
formHelper.expectError('image_size', 'pattern');
});
-
it('should give error on 0 image size', () => {
formHelper.setValue('image_size', 0);
component.onSubmit();
import { PoolService } from '~/app/shared/api/pool.service';
import { RbdService } from '~/app/shared/api/rbd.service';
import { FormatterService } from '~/app/shared/services/formatter.service';
-import { Observable } from 'rxjs';
+import { forkJoin, Observable } from 'rxjs';
import { CdValidators } from '~/app/shared/forms/cd-validators';
import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { HttpResponse } from '@angular/common/http';
@Component({
selector: 'cd-nvmeof-namespaces-form',
currentBytes: number;
invalidSizeError: boolean;
group: string;
+ MAX_NAMESPACE_CREATE: number = 5;
+ MIN_NAMESPACE_CREATE: number = 1;
+ requiredInvalidText: string = $localize`This field is required`;
+ nsCountInvalidText: string = $localize`The namespace count should be between 1 and 5`;
constructor(
public actionLabels: ActionLabelsI18n,
this.poolService.getList().subscribe((resp: Pool[]) => {
this.rbdPools = resp.filter(this.rbdService.isRBDPool);
});
+ if (this.rbdPools?.length) {
+ this.nsForm.get('pool').setValue(this.rbdPools[0].pool_name);
+ }
}
ngOnInit() {
createForm() {
this.nsForm = new CdFormGroup({
- image: new UntypedFormControl(`nvme_ns_image:${Date.now()}`, {
- validators: [Validators.required, Validators.pattern(/^[^@/]+?$/)]
- }),
pool: new UntypedFormControl(null, {
validators: [Validators.required]
}),
image_size: new UntypedFormControl(1, [CdValidators.number(false), Validators.min(1)]),
- unit: new UntypedFormControl(this.units[2])
+ unit: new UntypedFormControl(this.units[2]),
+ nsCount: new UntypedFormControl(this.MAX_NAMESPACE_CREATE, [
+ Validators.required,
+ Validators.max(this.MAX_NAMESPACE_CREATE),
+ Validators.min(this.MIN_NAMESPACE_CREATE)
+ ])
});
}
- updateRequest(rbdImageSize: number): NamespaceUpdateRequest {
+ buildUpdateRequest(rbdImageSize: number): Observable<HttpResponse<Object>> {
const request: NamespaceUpdateRequest = {
gw_group: this.group,
rbd_image_size: rbdImageSize
};
- return request;
+ return this.nvmeofService.updateNamespace(
+ this.subsystemNQN,
+ this.nsid,
+ request as NamespaceUpdateRequest
+ );
}
- createRequest(rbdImageSize: number): NamespaceCreateRequest {
- const image = this.nsForm.getValue('image');
- const pool = this.nsForm.getValue('pool');
- const request: NamespaceCreateRequest = {
- gw_group: this.group,
- rbd_image_name: image,
- rbd_pool: pool
- };
- if (rbdImageSize) {
- request['rbd_image_size'] = rbdImageSize;
- }
- return request;
+ randomString() {
+ return Math.random().toString(36).substring(2);
}
- buildRequest() {
- const image_size = this.nsForm.getValue('image_size');
- let rbdImageSize: number = null;
- if (image_size) {
- const image_size_unit = this.nsForm.getValue('unit');
- const value: number = this.formatterService.toBytes(image_size + image_size_unit);
- rbdImageSize = value;
- }
- if (this.edit) {
- return this.updateRequest(rbdImageSize);
- } else {
- return this.createRequest(rbdImageSize);
+ buildCreateRequest(rbdImageSize: number, nsCount: number): Observable<HttpResponse<Object>>[] {
+ const pool = this.nsForm.getValue('pool');
+ const requests: Observable<HttpResponse<Object>>[] = [];
+
+ for (let i = 1; i <= nsCount; i++) {
+ const request: NamespaceCreateRequest = {
+ gw_group: this.group,
+ rbd_image_name: `nvme_${pool}_${this.group}_${this.randomString()}`,
+ rbd_pool: pool
+ };
+ if (rbdImageSize) {
+ request['rbd_image_size'] = rbdImageSize;
+ }
+ requests.push(this.nvmeofService.createNamespace(this.subsystemNQN, request));
}
+
+ return requests;
}
validateSize() {
this.invalidSizeError = false;
const component = this;
const taskUrl: string = `nvmeof/namespace/${this.edit ? URLVerbs.EDIT : URLVerbs.CREATE}`;
- const request: NamespaceCreateRequest | NamespaceUpdateRequest = this.buildRequest();
- let action: Observable<any>;
+ const image_size = this.nsForm.getValue('image_size');
+ const nsCount = this.nsForm.getValue('nsCount');
+ let action: Observable<HttpResponse<Object>>;
+ let rbdImageSize: number = null;
+ if (image_size) {
+ const image_size_unit = this.nsForm.getValue('unit');
+ const value: number = this.formatterService.toBytes(image_size + image_size_unit);
+ rbdImageSize = value;
+ }
if (this.edit) {
action = this.taskWrapperService.wrapTaskAroundCall({
task: new FinishedTask(taskUrl, {
nqn: this.subsystemNQN,
nsid: this.nsid
}),
- call: this.nvmeofService.updateNamespace(this.subsystemNQN, this.nsid, request)
+ call: this.buildUpdateRequest(rbdImageSize)
});
} else {
action = this.taskWrapperService.wrapTaskAroundCall({
task: new FinishedTask(taskUrl, {
- nqn: this.subsystemNQN
+ nqn: this.subsystemNQN,
+ nsCount
}),
- call: this.nvmeofService.createNamespace(
- this.subsystemNQN,
- request as NamespaceCreateRequest
- )
+ call: forkJoin(this.buildCreateRequest(rbdImageSize, nsCount))
});
}
name: 'pluralize'
})
export class PluralizePipe implements PipeTransform {
- transform(value: string): string {
+ transform(value: string, count?: number): string {
+ if (count <= 1) {
+ return value;
+ }
if (value.endsWith('y')) {
return value.slice(0, -1) + 'ies';
} else {
import { FinishedTask } from '../models/finished-task';
import { ImageSpec } from '../models/image-spec';
import { Task } from '../models/task';
+import { PluralizePipe } from '../pipes/pluralize.pipe';
export class TaskMessageOperation {
running: string;
}
);
+ pluralize = new PluralizePipe().transform;
+
commonOperations = {
create: new TaskMessageOperation($localize`Creating`, $localize`create`, $localize`Created`),
update: new TaskMessageOperation($localize`Updating`, $localize`update`, $localize`Updated`),
return $localize`listener '${metadata.host_name} for subsystem ${metadata.nqn}`;
}
- nvmeofNamespace(metadata: any) {
+ nvmeofNamespace(metadata: { nqn: string; nsCount?: number; nsid?: string }) {
if (metadata?.nsid) {
return $localize`namespace ${metadata.nsid} for subsystem '${metadata.nqn}'`;
}
- return $localize`namespace for subsystem '${metadata.nqn}'`;
+ return $localize`${metadata.nsCount} ${this.pluralize(
+ 'namespace',
+ metadata.nsCount
+ )} for subsystem '${metadata.nqn}'`;
}
- nvmeofInitiator(metadata: any) {
- return $localize`initiator${metadata?.plural ? 's' : ''} for subsystem ${metadata.nqn}`;
+ nvmeofInitiator(metadata: { plural: number; nqn: string }) {
+ return $localize`${this.pluralize('initiator', metadata.plural)} for subsystem ${metadata.nqn}`;
}
nfs(metadata: any) {