From: Sagar Gopale
Date: Mon, 23 Mar 2026 06:08:44 +0000 (+0530)
Subject: mgr/dashboard: Add Hosts via CSV Upload
X-Git-Tag: testing/wip-yuri10-testing-20260526.155424-main~5^2
X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=de23df56d1047055db2c3602893160513a42ca52;p=ceph-ci.git
mgr/dashboard: Add Hosts via CSV Upload
Fixes: https://tracker.ceph.com/issues/75578
Signed-off-by: Sagar Gopale
---
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
index c575ca7d01b..085ae2f04de 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
@@ -61,6 +61,7 @@ import {
CheckboxModule,
ComboBoxModule,
DatePickerModule,
+ FileUploaderModule,
GridModule,
IconModule,
IconService,
@@ -127,6 +128,7 @@ import { NvmeofTabsComponent } from './nvmeof-tabs/nvmeof-tabs.component';
NumberModule,
ModalModule,
DatePickerModule,
+ FileUploaderModule,
ComboBoxModule,
TabsModule,
TagModule,
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-2/nvmeof-subsystem-step-2.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-2/nvmeof-subsystem-step-2.component.html
index b979d1c0fd5..a733709627f 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-2/nvmeof-subsystem-step-2.component.html
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-2/nvmeof-subsystem-step-2.component.html
@@ -60,7 +60,7 @@
class="form-item">
Add hosts
+ i18n>Add host manually
} @else {
{
let component: NvmeofSubsystemsStepTwoComponent;
@@ -26,6 +32,7 @@ describe('NvmeofSubsystemsStepTwoComponent', () => {
ReactiveFormsModule,
RouterTestingModule,
SharedModule,
+ FileUploaderModule,
InputModule,
GridModule,
RadioModule,
@@ -111,4 +118,82 @@ describe('NvmeofSubsystemsStepTwoComponent', () => {
expect(component.addedHostsLength).toBe(0);
});
});
+
+ describe('processCsvContent', () => {
+ it('should import valid host NQNs from CSV', () => {
+ const csvContent = `
+host_nqn
+nqn.2023-01.com.example:host1
+nqn.2023-01.com.example:host2
+`;
+
+ component.processCsvContent(csvContent);
+
+ expect(form.get('addedHosts')?.value).toEqual([
+ 'nqn.2023-01.com.example:host1',
+ 'nqn.2023-01.com.example:host2'
+ ]);
+ expect(component.addedHostsLength).toBe(2);
+ expect(component.csvUploadError).toBe('');
+ });
+
+ it('should skip duplicates and invalid values from CSV', () => {
+ form.get('addedHosts')?.setValue(['nqn.2023-01.com.example:existing']);
+ component.existingHosts = ['nqn.2023-01.com.example:already-there'];
+
+ const csvContent = `
+host_nqn
+nqn.2023-01.com.example:existing
+nqn.2023-01.com.example:new-host
+invalid-host
+nqn.2023-01.com.example:already-there
+nqn.2023-01.com.example:new-host
+`;
+
+ component.processCsvContent(csvContent);
+
+ expect(form.get('addedHosts')?.value).toEqual([
+ 'nqn.2023-01.com.example:existing',
+ 'nqn.2023-01.com.example:new-host'
+ ]);
+ expect(component.addedHostsLength).toBe(2);
+ expect(component.csvUploadError).toBe('');
+ });
+
+ it('should show error when csv has no valid hosts', () => {
+ const csvContent = `
+host_nqn
+invalid-host
+another-invalid-host
+`;
+
+ component.processCsvContent(csvContent);
+
+ expect(form.get('addedHosts')?.value).toEqual([]);
+ expect(component.csvUploadError).toBe('No valid hosts found in the CSV file.');
+ });
+
+ it('should not show invalid-csv error when all uploaded hosts are duplicates', () => {
+ form
+ .get('addedHosts')
+ ?.setValue(['nqn.2023-01.com.example:host1', 'nqn.2023-01.com.example:host2']);
+ component.addedHostsLength = 2;
+
+ const csvContent = `
+host_nqn
+nqn.2023-01.com.example:host1
+nqn.2023-01.com.example:host2
+`;
+
+ component.processCsvContent(csvContent);
+
+ expect(form.get('addedHosts')?.value).toEqual([
+ 'nqn.2023-01.com.example:host1',
+ 'nqn.2023-01.com.example:host2'
+ ]);
+ expect(component.csvUploadError).toBe(
+ 'No new hosts were found. Provided hosts are already in place.'
+ );
+ });
+ });
});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-2/nvmeof-subsystem-step-2.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-2/nvmeof-subsystem-step-2.component.ts
index f90b0fa2757..70cfbd75a31 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-2/nvmeof-subsystem-step-2.component.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-2/nvmeof-subsystem-step-2.component.ts
@@ -2,6 +2,8 @@ import { Component, Input, OnInit, TemplateRef, ViewChild, ViewEncapsulation } f
import { FormControl, UntypedFormControl } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { FileUploaderService } from '~/app/shared/services/file-uploader.service';
+
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
import { CdValidators } from '~/app/shared/forms/cd-validators';
@@ -30,11 +32,18 @@ export class NvmeofSubsystemsStepTwoComponent implements OnInit, TearsheetStep {
};
HOST_TYPE = HOST_TYPE;
addedHostsLength: number = 0;
+ csvUploadError = '';
+ csvDropText: string = $localize`Drag and drop files here or click to upload`;
NQN_REGEX = /^nqn\.(19|20)\d\d-(0[1-9]|1[0-2])\.\D{2,3}(\.[A-Za-z0-9-]+)+(:[A-Za-z0-9-\.]+(:[A-Za-z0-9-\.]+)*)$/;
NQN_REGEX_UUID = /^nqn\.2014-08\.org\.nvmexpress:uuid:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
ALLOW_ALL_HOST = '*';
+ uploadedHosts = new Set();
- constructor(public actionLabels: ActionLabelsI18n, public activeModal: NgbActiveModal) {}
+ constructor(
+ public actionLabels: ActionLabelsI18n,
+ public activeModal: NgbActiveModal,
+ private fileUploaderService: FileUploaderService
+ ) {}
ngOnInit() {
this.createForm();
@@ -100,6 +109,82 @@ export class NvmeofSubsystemsStepTwoComponent implements OnInit, TearsheetStep {
}
}
+ onCsvUpload(files: Set