]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add RADOS Namespace Support in NVMe-oF UI 70274/head
authorpujashahu <pshahu@redhat.com>
Fri, 17 Jul 2026 08:41:06 +0000 (14:11 +0530)
committerpujashahu <pshahu@redhat.com>
Wed, 22 Jul 2026 08:39:39 +0000 (14:09 +0530)
Fixes: https://tracker.ceph.com/issues/78330
Signed-off-by: pujaoshahu <pshahu@redhat.com>
src/pybind/mgr/dashboard/controllers/rbd.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-form/nvmeof-namespaces-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-form/nvmeof-namespaces-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-form/nvmeof-namespaces-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-list/nvmeof-namespaces-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-list/nvmeof-namespaces-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-namespaces-list/nvmeof-namespaces-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts
src/pybind/mgr/dashboard/openapi.yaml

index 1496d57865b9991f6c7a41276a1d0ae6af4a6483..344252163df48bc3a2d03494cd71e810cf02ebde 100644 (file)
@@ -82,14 +82,15 @@ class Rbd(RESTController):
 
     DEFAULT_LIMIT = 5
 
-    def _rbd_list(self, pool_name=None, offset=0, limit=DEFAULT_LIMIT, search='', sort=''):
+    def _rbd_list(self, pool_name=None, namespace=None, offset=0, limit=DEFAULT_LIMIT,
+                  search='', sort=''):
         if pool_name:
             pools = [pool_name]
         else:
             pools = [p['pool_name'] for p in CephService.get_pool_list('rbd')]
 
         images, num_total_images = RbdService.rbd_pool_list(
-            pools, offset=offset, limit=limit, search=search, sort=sort)
+            pools, namespace=namespace, offset=offset, limit=limit, search=search, sort=sort)
         cherrypy.response.headers['X-Total-Count'] = num_total_images
         pool_result = {}
         for i, image in enumerate(images):
@@ -111,14 +112,16 @@ class Rbd(RESTController):
     @EndpointDoc("Display Rbd Images",
                  parameters={
                      'pool_name': (str, 'Pool Name'),
+                     'namespace': (str, 'Optional RBD namespace. If provided, list images only '
+                                        'from this namespace within the selected pool(s).'),
                      'limit': (int, 'limit'),
                      'offset': (int, 'offset'),
                  },
                  responses={200: RBD_SCHEMA})
     @RESTController.MethodMap(version=APIVersion(2, 0))  # type: ignore
-    def list(self, pool_name=None, offset: int = 0, limit: int = DEFAULT_LIMIT,
+    def list(self, pool_name=None, namespace=None, offset: int = 0, limit: int = DEFAULT_LIMIT,
              search: str = '', sort: str = ''):
-        return self._rbd_list(pool_name, offset=int(offset), limit=int(limit),
+        return self._rbd_list(pool_name, namespace=namespace, offset=int(offset), limit=int(limit),
                               search=search, sort=sort)
 
     @handle_rbd_error()
index c4beb493e6163789628a5efb4d116ccd5b4955e9..43d658dfdfcfbcdecf7610dd9aa3807a6db27d15 100644 (file)
         </div>
       </div>
 
+      <!-- Namespace (optional) -->
+      <div
+        cdsRow
+        class="form-item"
+      >
+        <div cdsCol>
+          <cds-select
+            formControlName="rados_namespace"
+            label="RADOS Namespace"
+            helperText="Namespace where the RBD image resides."
+            i18n-label
+            i18n-helperText
+            cdOptionalField="RADOS Namespace"
+          >
+            <option
+              [ngValue]="null"
+              selected
+            >
+              Select a namespace
+            </option>
+            @if (radosNamespaces === null && nsForm.getValue('pool')) {
+              <option
+                disabled
+                [ngValue]="null"
+              >
+                Loading...
+              </option>
+            }
+            @for (ns of radosNamespaces; track ns.namespace) {
+              <option [value]="ns.namespace">{{ ns.namespace }}</option>
+            }
+          </cds-select>
+        </div>
+      </div>
+
       <!-- RBD image creation (UI only) -->
       <div
         cdsRow
index ef1785e33b66e0db4d53ec84513e70dc563eabbd..980d4feec30cd34d9ad840e884595d77cbc5c15d 100644 (file)
@@ -12,11 +12,13 @@ import { NvmeofNamespacesFormComponent } from './nvmeof-namespaces-form.componen
 import { FormHelper, Mocks } from '~/testing/unit-test-helper';
 import { FormatterService } from '~/app/shared/services/formatter.service';
 import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { RbdService } from '~/app/shared/api/rbd.service';
 import { of, Observable } from 'rxjs';
 import { PoolService } from '~/app/shared/api/pool.service';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 import { NumberModule, RadioModule, ComboBoxModule, SelectModule } from 'carbon-components-angular';
 import { ActivatedRoute, Router } from '@angular/router';
+import { RadosNamespace } from '~/app/shared/models/nvmeof';
 
 import { ActivatedRouteStub } from '~/testing/activated-route-stub';
 
@@ -25,6 +27,7 @@ const MOCK_POOLS = [
   Mocks.getPool('rbd', 2),
   Mocks.getPool('pool-2', 3)
 ];
+
 class MockPoolsService {
   getList() {
     return of(MOCK_POOLS);
@@ -59,10 +62,47 @@ const MOCK_ROUTER = {
   createUrl: 'https://192.168.100.100:8443/#/block/nvmeof/subsystems/(modal:create)?group=default'
 };
 
+const MOCK_RADOS_NAMESPACES: RadosNamespace[] = [
+  { namespace: 'tenant-1', num_images: 2 },
+  { namespace: 'tenant-2', num_images: 0 }
+];
+
+const MOCK_RBD_IMAGES_DEFAULT_NS = [
+  { name: 'img-alpha', size: 1073741824 },
+  { name: 'img-beta', size: 2147483648 }
+];
+
+const MOCK_RBD_IMAGES_TENANT1 = [
+  { name: 'img-alpha', size: 1073741824 }, // same name as in default ns — must stay selectable
+  { name: 'img-gamma', size: 536870912 }
+];
+
+/**
+ * Builds a minimal namespace shape for fetchUsedImages() tests.
+ */
+function makeUsedNs(pool: string, imageName: string, radosNs: string = '') {
+  return {
+    nsid: 1,
+    uuid: 'uuid',
+    bdev_name: 'bdev',
+    rbd_image_name: imageName,
+    rbd_pool_name: pool,
+    rados_namespace_name: radosNs,
+    load_balancing_group: 1,
+    rbd_image_size: 1073741824,
+    block_size: 512,
+    rw_ios_per_second: 0,
+    rw_mbytes_per_second: 0,
+    r_mbytes_per_second: 0,
+    w_mbytes_per_second: 0
+  };
+}
+
 describe('NvmeofNamespacesFormComponent', () => {
   let component: NvmeofNamespacesFormComponent;
   let fixture: ComponentFixture<NvmeofNamespacesFormComponent>;
   let nvmeofService: NvmeofService;
+  let rbdService: RbdService;
   let router: Router;
   let form: CdFormGroup;
   let formHelper: FormHelper;
@@ -97,6 +137,7 @@ describe('NvmeofNamespacesFormComponent', () => {
         SelectModule
       ]
     }).compileComponents();
+
     fixture = TestBed.createComponent(NvmeofNamespacesFormComponent);
     component = fixture.componentInstance;
   });
@@ -104,26 +145,34 @@ describe('NvmeofNamespacesFormComponent', () => {
   it('should create component', () => {
     expect(component).toBeTruthy();
   });
-  describe('should test create form', () => {
+
+  describe('create form', () => {
     beforeEach(() => {
       router = TestBed.inject(Router);
       nvmeofService = TestBed.inject(NvmeofService);
+      rbdService = TestBed.inject(RbdService);
+
       spyOn(nvmeofService, 'createNamespace').and.returnValue(
         of(new HttpResponse({ body: MOCK_NS_RESPONSE }))
       );
-
       spyOn(nvmeofService, 'getInitiators').and.returnValue(
         of([{ nqn: 'host1' }, { nqn: 'host2' }])
       );
+      spyOn(rbdService, 'listNamespaces').and.returnValue(of(MOCK_RADOS_NAMESPACES));
+      // Default: no existing namespaces. Individual tests can override via
+      // (nvmeofService.listNamespaces as jasmine.Spy).and.returnValue(...)
+      spyOn(nvmeofService, 'listNamespaces').and.returnValue(of({ namespaces: [] }));
       spyOn(component, 'randomString').and.returnValue(MOCK_RANDOM_STRING);
+
       Object.defineProperty(router, 'url', {
         get: jasmine.createSpy('url').and.returnValue(MOCK_ROUTER.createUrl)
       });
+
       component.ngOnInit();
       form = component.nsForm;
       formHelper = new FormHelper(form);
-      formHelper.setValue('pool', 'rbd');
     });
+
     it('should call createNamespace on submit with specific hosts', () => {
       formHelper.setValue('pool', 'rbd');
       formHelper.setValue('image_size', new FormatterService().toBytes('1GiB'));
@@ -145,5 +194,122 @@ describe('NvmeofNamespacesFormComponent', () => {
       const request = (nvmeofService.createNamespace as jasmine.Spy).calls.mostRecent().args[1];
       expect(request.block_size).toBe(1024);
     });
+
+    describe('RADOS namespace field', () => {
+      it('should load RADOS namespaces when a pool is selected', () => {
+        formHelper.setValue('pool', 'rbd');
+
+        expect(rbdService.listNamespaces).toHaveBeenCalledWith('rbd');
+        expect(component.radosNamespaces).toEqual(MOCK_RADOS_NAMESPACES);
+      });
+
+      it('should clear rados_namespace selection and reload namespaces when pool changes', () => {
+        formHelper.setValue('pool', 'rbd');
+        formHelper.setValue('rados_namespace', 'tenant-1');
+
+        // Switching to a different pool must clear the selected namespace
+        formHelper.setValue('pool', 'pool-2');
+
+        expect(form.getValue('rados_namespace')).toBeNull();
+        // listNamespaces should have been called for the new pool
+        expect(rbdService.listNamespaces).toHaveBeenCalledWith('pool-2');
+      });
+
+      it('should not include rados_namespace in the create request when none is selected', () => {
+        formHelper.setValue('pool', 'rbd');
+        formHelper.setValue('image_size', new FormatterService().toBytes('1GiB'));
+        formHelper.setValue('subsystem', MOCK_SUBSYSTEM);
+
+        component.onSubmit();
+
+        const request = (nvmeofService.createNamespace as jasmine.Spy).calls.mostRecent().args[1];
+        expect(request.rados_namespace).toBeUndefined();
+      });
+
+      it('should include rados_namespace in the create request when a namespace is selected', () => {
+        formHelper.setValue('pool', 'rbd');
+        formHelper.setValue('rados_namespace', 'tenant-1');
+        formHelper.setValue('image_size', new FormatterService().toBytes('1GiB'));
+        formHelper.setValue('subsystem', MOCK_SUBSYSTEM);
+
+        component.onSubmit();
+
+        const request = (nvmeofService.createNamespace as jasmine.Spy).calls.mostRecent().args[1];
+        expect(request.rados_namespace).toBe('tenant-1');
+      });
+    });
+
+    describe('image filtering with RADOS namespace isolation', () => {
+      /**
+       * The NvmeofService and RbdService are singletons in TestBed DI.  The parent
+       * beforeEach has already called spyOn() on createNamespace, getInitiators and
+       * listNamespaces (RBD). Here we only need to override the two methods that
+       * control which images are "used" and which are available: listNamespaces and
+       * rbdService.list.  We create a new component instance so that ngOnInit() picks
+       * up the configured listNamespaces stub when it calls fetchUsedImages().
+       */
+      function buildComponentWithStubs(
+        usedNamespaces: ReturnType<typeof makeUsedNs>[],
+        rbdListResponse: { pool_name: string; value: { name: string; size: number }[] }[]
+      ): void {
+        // Re-spy the two methods that vary between test cases
+        (nvmeofService.listNamespaces as jasmine.Spy).and.returnValue(
+          of({ namespaces: usedNamespaces })
+        );
+
+        fixture = TestBed.createComponent(NvmeofNamespacesFormComponent);
+        component = fixture.componentInstance;
+
+        // rbdService is injected into the component — spy before ngOnInit
+        spyOn(component['rbdService'], 'list').and.returnValue(of(rbdListResponse));
+
+        component.ngOnInit();
+        form = component.nsForm;
+        formHelper = new FormHelper(form);
+      }
+
+      it('should treat the same image name in different RADOS namespaces as independently available', () => {
+        // 'img-alpha' is recorded as used in the **default** namespace of pool 'rbd'
+        buildComponentWithStubs(
+          [makeUsedNs('rbd', 'img-alpha', '')],
+          [{ pool_name: 'rbd', value: MOCK_RBD_IMAGES_TENANT1 }]
+        );
+
+        formHelper.setValue('pool', 'rbd');
+        formHelper.setValue('rados_namespace', 'tenant-1');
+
+        // img-alpha exists in tenant-1's image list AND is NOT used in tenant-1,
+        // so it must remain available for selection.
+        expect(component.rbdImages.map((i) => i.name)).toContain('img-alpha');
+      });
+
+      it('should exclude images already used in the same pool + RADOS namespace', () => {
+        // 'img-alpha' is recorded as used in 'tenant-1'
+        buildComponentWithStubs(
+          [makeUsedNs('rbd', 'img-alpha', 'tenant-1')],
+          [{ pool_name: 'rbd', value: MOCK_RBD_IMAGES_TENANT1 }]
+        );
+
+        formHelper.setValue('pool', 'rbd');
+        formHelper.setValue('rados_namespace', 'tenant-1');
+
+        expect(component.rbdImages.map((i) => i.name)).not.toContain('img-alpha');
+        expect(component.rbdImages.map((i) => i.name)).toContain('img-gamma');
+      });
+
+      it('should exclude used images in the default namespace when no RADOS namespace is selected', () => {
+        // 'img-alpha' is recorded as used in the default namespace (empty radosNs)
+        buildComponentWithStubs(
+          [makeUsedNs('rbd', 'img-alpha', '')],
+          [{ pool_name: 'rbd', value: MOCK_RBD_IMAGES_DEFAULT_NS }]
+        );
+
+        formHelper.setValue('pool', 'rbd');
+        // rados_namespace stays null → scoped to the default namespace
+
+        expect(component.rbdImages.map((i) => i.name)).not.toContain('img-alpha');
+        expect(component.rbdImages.map((i) => i.name)).toContain('img-beta');
+      });
+    });
   });
 });
index b5b8fb266470f00dd8cede9dde41a54830a1b05d..04f3a53af91e6da0cd672daf27bca5ca3486a520 100644 (file)
@@ -10,6 +10,7 @@ import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants
 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
 import { FinishedTask } from '~/app/shared/models/finished-task';
 import {
+  RadosNamespace,
   NvmeofSubsystem,
   NvmeofSubsystemInitiator,
   NvmeofSubsystemNamespace
@@ -48,6 +49,7 @@ export class NvmeofNamespacesFormComponent implements OnInit {
   subsystems: NvmeofSubsystem[] | null = null;
   rbdPools: Array<Pool> = null;
   rbdImages: any[] = [];
+  radosNamespaces: RadosNamespace[] | null = null;
   initiatorCandidates: { content: string; selected: boolean }[] = [];
 
   nsid: string;
@@ -146,17 +148,46 @@ export class NvmeofNamespacesFormComponent implements OnInit {
     }
   }
 
-  // Stores all RBD images fetched for the selected pool
+  // Stores all RBD images fetched for the selected pool + RADOS namespace.
   private allRbdImages: { name: string; size: number }[] = [];
-  // Maps pool name to a Set of used image names for O(1) lookup
+  // Maps "pool:radosNamespace" composite key to used image names.
   private usedRbdImages: Map<string, Set<string>> = new Map();
 
   onPoolChange(): void {
     const pool = this.nsForm.getValue('pool');
     if (!pool) return;
 
+    this.radosNamespaces = null;
+    this.nsForm.get('rados_namespace').setValue(null, { emitEvent: false });
+
+    this.rbdService.listNamespaces(pool).subscribe({
+      next: (namespaces) => {
+        this.radosNamespaces = namespaces as RadosNamespace[];
+      },
+      error: () => {
+        this.radosNamespaces = [];
+      }
+    });
+
+    this.fetchImagesForCurrentSelection(pool, null);
+  }
+
+  private onRadosNamespaceChange(): void {
+    const pool = this.nsForm.getValue('pool');
+    if (!pool) return;
+
+    const radosNs = this.nsForm.getValue('rados_namespace') as string | null;
+    this.fetchImagesForCurrentSelection(pool, radosNs);
+  }
+
+  private fetchImagesForCurrentSelection(pool: string, radosNs: string | null): void {
+    const params: Record<string, string> = { pool_name: pool, offset: '0', limit: '-1' };
+    if (radosNs) {
+      params['namespace'] = radosNs;
+    }
+
     this.rbdService
-      .list({ pool_name: pool, offset: '0', limit: '-1' })
+      .list(params)
       .subscribe((pools: { pool_name: string; value: { name: string; size: number }[] }[]) => {
         const selectedPool = pools.find((p) => p.pool_name === pool);
         this.allRbdImages = selectedPool?.value ?? [];
@@ -180,10 +211,11 @@ export class NvmeofNamespacesFormComponent implements OnInit {
         ? response
         : (response?.namespaces ?? []);
       this.usedRbdImages = namespaces.reduce((map, ns) => {
-        if (!map.has(ns.rbd_pool_name)) {
-          map.set(ns.rbd_pool_name, new Set<string>());
+        const key = this.usedImagesKey(ns.rbd_pool_name, ns.rados_namespace_name ?? '');
+        if (!map.has(key)) {
+          map.set(key, new Set<string>());
         }
-        map.get(ns.rbd_pool_name)!.add(ns.rbd_image_name);
+        map.get(key)!.add(ns.rbd_image_name);
         return map;
       }, new Map<string, Set<string>>());
       this.filterImages();
@@ -219,17 +251,23 @@ export class NvmeofNamespacesFormComponent implements OnInit {
       this.rbdImages = [];
       return;
     }
-    const usedInPool = this.usedRbdImages.get(pool);
-    this.rbdImages = usedInPool
-      ? this.allRbdImages.filter((img) => !usedInPool.has(img.name))
+    const radosNs = (this.nsForm.getValue('rados_namespace') as string | null) ?? '';
+    const usedInScope = this.usedRbdImages.get(this.usedImagesKey(pool, radosNs));
+    this.rbdImages = usedInScope
+      ? this.allRbdImages.filter((img) => !usedInScope.has(img.name))
       : [...this.allRbdImages];
   }
 
+  private usedImagesKey(pool: string, radosNs: string): string {
+    return `${pool}\x00${radosNs}`;
+  }
+
   createForm() {
     this.nsForm = new CdFormGroup({
       pool: new UntypedFormControl('', {
         validators: [Validators.required]
       }),
+      rados_namespace: new UntypedFormControl(null),
       subsystem: new UntypedFormControl('', {
         validators: [Validators.required]
       }),
@@ -258,6 +296,10 @@ export class NvmeofNamespacesFormComponent implements OnInit {
       this.onPoolChange();
     });
 
+    this.nsForm.get('rados_namespace').valueChanges.subscribe(() => {
+      this.onRadosNamespaceChange();
+    });
+
     this.nsForm.get('nsCount').valueChanges.subscribe((count: number) => {
       if (count > 1) {
         const creationControl = this.nsForm.get('rbd_image_creation');
@@ -324,6 +366,7 @@ export class NvmeofNamespacesFormComponent implements OnInit {
     noAutoVisible: boolean
   ): Observable<HttpResponse<Object>>[] {
     const pool = this.nsForm.getValue('pool');
+    const radosNs = this.nsForm.getValue('rados_namespace') as string | null;
     const requests: Observable<HttpResponse<Object>>[] = [];
     const creationMode = this.nsForm.getValue('rbd_image_creation');
     const isGatewayProvisioned = creationMode === 'gateway_provisioned';
@@ -340,6 +383,9 @@ export class NvmeofNamespacesFormComponent implements OnInit {
         no_auto_visible: noAutoVisible
       };
 
+      if (radosNs) {
+        request.rados_namespace = radosNs;
+      }
       if (blockSize) {
         request.block_size = blockSize;
       }
index b38d58bec2722e4da5c24982819c2f8aba73278b..e130e98a7f46854372bbaa9b71b8ab13cc405e16 100644 (file)
@@ -5,7 +5,7 @@
         [data]="namespaces"
         [compactSearchField]="true"
         columnMode="flex"
-        (fetchData)="fetchData()"
+        (fetchData)="listNamespaces()"
         [columns]="namespacesColumns"
         identifier="unique_id"
         [forceIdentifier]="true"
index 48021924dd8d66290f02cffc0215250882799d7c..864b422518a2042003eef9e94a369581d534446b 100644 (file)
@@ -154,7 +154,7 @@ describe('NvmeofNamespacesListComponent', () => {
       );
       done();
     });
-    component.fetchData();
+    component.listNamespaces();
   });
 
   it('should open delete modal with correct data', () => {
@@ -192,16 +192,17 @@ describe('NvmeofNamespacesListComponent', () => {
       done();
     });
 
-    component.fetchData();
+    component.listNamespaces();
   });
 
   it('should update group and trigger namespace fetch on group change', () => {
-    const fetchDataSpy = jest.spyOn(component, 'fetchData');
+    const listNamespacesSpy = jest.spyOn(component, 'listNamespaces');
+    listNamespacesSpy.mockClear();
 
     component.onGroupChange('g1');
 
     expect(component.group).toBe('g1');
-    expect(fetchDataSpy).not.toHaveBeenCalled(); // onGroupChange calls namespaceSubject.next() directly
+    expect(listNamespacesSpy).toHaveBeenCalled();
   });
 
   it('should clear group on onGroupChange with null', () => {
index 427dd6068947596d76ad7bbb167be7d905764578..4a8283eed5553e54be6b24df4a85f687d64c5c72 100644 (file)
@@ -81,6 +81,11 @@ export class NvmeofNamespacesListComponent implements OnInit, OnDestroy {
         name: $localize`Pool`,
         prop: 'rbd_pool_name'
       },
+      {
+        name: $localize`RADOS Namespace`,
+        prop: 'rados_namespace_name',
+        flexGrow: 1
+      },
       {
         name: $localize`Image`,
         prop: 'rbd_image_name'
@@ -160,7 +165,7 @@ export class NvmeofNamespacesListComponent implements OnInit, OnDestroy {
     );
     this.nvmeofStateService.refresh$
       .pipe(takeUntil(this.destroy$))
-      .subscribe(() => this.fetchData());
+      .subscribe(() => this.listNamespaces());
   }
 
   private normalizeAndDedup(
@@ -244,10 +249,6 @@ export class NvmeofNamespacesListComponent implements OnInit, OnDestroy {
     this.namespaceSubject.next();
   }
 
-  fetchData() {
-    this.listNamespaces();
-  }
-
   private setTableLoading(loading: boolean): void {
     if (this.table) {
       this.table.loadingIndicator = loading;
index fb3bb1c9edefe2f7f0fc42f6247bc649b88a954c..a6a03cc9812c960122f006ba72cf07181e8ae930 100644 (file)
@@ -11,11 +11,11 @@ import {
   NvmeofSubsystem,
   NvmeofSubsystemNamespace
 } from '../models/nvmeof';
-import { HostService } from './host.service';
-import { OrchestratorService } from './orchestrator.service';
 import { HostStatus } from '../enum/host-status.enum';
 import { Host } from '../models/host.interface';
 import { OrchestratorStatus } from '../models/orchestrator.interface';
+import { HostService } from './host.service';
+import { OrchestratorService } from './orchestrator.service';
 
 export type SetupState = {
   hasGatewayGroups: boolean;
@@ -46,6 +46,7 @@ export type ListenerRequest = NvmeofRequest & {
 export type NamespaceCreateRequest = NvmeofRequest & {
   rbd_image_name?: string;
   rbd_pool: string;
+  rados_namespace?: string;
   rbd_image_size?: number;
   no_auto_visible?: boolean;
   block_size?: number;
index 3316570ebd366d925cbfaebbc6f10848d8495063..810c4da720be3b05a3c1e3c7450d86587d44f417 100644 (file)
@@ -53,6 +53,7 @@ export interface NvmeofSubsystemNamespace {
   bdev_name: string;
   rbd_image_name: string;
   rbd_pool_name: string;
+  rados_namespace_name?: string; // empty string or absent means default RADOS namespace
   load_balancing_group: number;
   rbd_image_size: number | string;
   block_size: number;
@@ -63,6 +64,11 @@ export interface NvmeofSubsystemNamespace {
   subsystem_nqn?: string; // Field from JSON (mapped from ns_subsystem_nqn if needed)
 }
 
+export type RadosNamespace = {
+  namespace: string;
+  num_images: number;
+};
+
 export interface NvmeofGatewayGroup extends CephServiceSpec {
   name: string;
   gatewayCount: {
index 4e0d2259ca8cca96297d6ae4896537cfa1223c4a..875962cb8ba1a54d128eebf2a74b95045d82aaf4 100644 (file)
@@ -265,6 +265,13 @@ paths:
         name: pool_name
         schema:
           type: string
+      - allowEmptyValue: true
+        description: Optional RBD namespace. If provided, list images only from this
+          namespace within the selected pool(s).
+        in: query
+        name: namespace
+        schema:
+          type: string
       - default: 0
         description: offset
         in: query