]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Create and delete listeners
authorAfreen Misbah <afreen23.git@gmail.com>
Wed, 17 Jul 2024 10:16:23 +0000 (15:46 +0530)
committerAfreen Misbah <afreen23.git@gmail.com>
Fri, 19 Jul 2024 09:07:06 +0000 (14:37 +0530)
Fixes https://tracker.ceph.com/issues/66996

- list listener under subsystems
- delete listener
- create listener

Signed-off-by: Afreen Misbah <afreen23.git@gmail.com>
(cherry picked from commit a4f2eefe21f973d532a7e08d1ce4e977f39dfe88)

 Conflicts:
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts

18 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-details/nvmeof-subsystems-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-details/nvmeof-subsystems-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts
src/pybind/mgr/dashboard/services/nvmeof_conf.py

index 8e926a40d99ed8c5dc465fd38e207807d9380bf8..21a0d5483ec79287eda0e2c11b9eaad69980f3ae 100644 (file)
@@ -43,6 +43,8 @@ import { NvmeofSubsystemsComponent } from './nvmeof-subsystems/nvmeof-subsystems
 import { NvmeofSubsystemsDetailsComponent } from './nvmeof-subsystems-details/nvmeof-subsystems-details.component';
 import { NvmeofTabsComponent } from './nvmeof-tabs/nvmeof-tabs.component';
 import { NvmeofSubsystemsFormComponent } from './nvmeof-subsystems-form/nvmeof-subsystems-form.component';
+import { NvmeofListenersFormComponent } from './nvmeof-listeners-form/nvmeof-listeners-form.component';
+import { NvmeofListenersListComponent } from './nvmeof-listeners-list/nvmeof-listeners-list.component';
 
 @NgModule({
   imports: [
@@ -87,7 +89,9 @@ import { NvmeofSubsystemsFormComponent } from './nvmeof-subsystems-form/nvmeof-s
     NvmeofSubsystemsComponent,
     NvmeofSubsystemsDetailsComponent,
     NvmeofTabsComponent,
-    NvmeofSubsystemsFormComponent
+    NvmeofSubsystemsFormComponent,
+    NvmeofListenersFormComponent,
+    NvmeofListenersListComponent
   ],
   exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent]
 })
@@ -244,6 +248,11 @@ const routes: Routes = [
             path: `${URLVerbs.EDIT}/:subsystem_nqn`,
             component: NvmeofSubsystemsFormComponent,
             outlet: 'modal'
+          },
+          {
+            path: `${URLVerbs.CREATE}/:subsystem_nqn/listener`,
+            component: NvmeofListenersFormComponent,
+            outlet: 'modal'
           }
         ]
       },
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.html
new file mode 100644 (file)
index 0000000..279d108
--- /dev/null
@@ -0,0 +1,76 @@
+<cd-modal [pageURL]="pageURL">
+  <span class="modal-title"
+        i18n>{{ action | titlecase }} {{ resource | upperFirst }}</span>
+  <ng-container class="modal-content">
+    <form name="listenerForm"
+          #formDir="ngForm"
+          [formGroup]="listenerForm"
+          novalidate>
+      <div class="modal-body">
+        <!-- Host -->
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="host">
+            <span class="required"
+                  i18n>Host Name</span>
+          </label>
+          <div class="cd-col-form-input">
+            <select id="host"
+                    name="host"
+                    class="form-select"
+                    formControlName="host">
+              <option *ngIf="hosts === null"
+                      [ngValue]="null"
+                      i18n>Loading...</option>
+              <option *ngIf="hosts && hosts.length === 0"
+                      [ngValue]="null"
+                      i18n>-- No hosts available --</option>
+              <option *ngIf="hosts && hosts.length > 0"
+                      [ngValue]="null"
+                      i18n>-- Select a host --</option>
+              <option *ngFor="let hostsItem of hosts"
+                      [ngValue]="hostsItem">{{ hostsItem.hostname }}</option>
+            </select>
+            <cd-help-text i18n>
+                This hostname uniquely identifies the gateway on which the listener is being set up.
+            </cd-help-text>
+            <span class="invalid-feedback"
+                  *ngIf="listenerForm.showError('host', formDir, 'required')"
+                  i18n>This field is required.</span>
+          </div>
+        </div>
+        <!-- Transport Service ID -->
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="trsvcid">
+            <span i18n>Transport Service ID</span>
+          </label>
+          <div class="cd-col-form-input">
+            <input id="trsvcid"
+                   class="form-control"
+                   type="text"
+                   name="trsvcid"
+                   formControlName="trsvcid">
+            <cd-help-text i18n>The IP port to use. Default is 4420.</cd-help-text>
+            <span class="invalid-feedback"
+                  *ngIf="listenerForm.showError('trsvcid', formDir, 'required')"
+                  i18n>This field is required.</span>
+            <span class="invalid-feedback"
+                  *ngIf="listenerForm.showError('trsvcid', formDir, 'max')"
+                  i18n>The value cannot be greated than 65535.</span>
+            <span class="invalid-feedback"
+                  *ngIf="listenerForm.showError('trsvcid', formDir, 'pattern')"
+                  i18n>The value must be a positive integer.</span>
+          </div>
+        </div>
+      </div>
+      <div class="modal-footer">
+        <div class="text-right">
+          <cd-form-button-panel (submitActionEvent)="onSubmit()"
+                                [form]="listenerForm"
+                                [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+        </div>
+      </div>
+    </form>
+  </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.spec.ts
new file mode 100644 (file)
index 0000000..b115fd5
--- /dev/null
@@ -0,0 +1,48 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { NvmeofListenersFormComponent } from './nvmeof-listeners-form.component';
+
+describe('NvmeofListenersFormComponent', () => {
+  let component: NvmeofListenersFormComponent;
+  let fixture: ComponentFixture<NvmeofListenersFormComponent>;
+  let nvmeofService: NvmeofService;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [NvmeofListenersFormComponent],
+      providers: [NgbActiveModal],
+      imports: [
+        HttpClientTestingModule,
+        NgbTypeaheadModule,
+        ReactiveFormsModule,
+        RouterTestingModule,
+        SharedModule,
+        ToastrModule.forRoot()
+      ]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(NvmeofListenersFormComponent);
+    component = fixture.componentInstance;
+    component.ngOnInit();
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  describe('should test form', () => {
+    beforeEach(() => {
+      nvmeofService = TestBed.inject(NvmeofService);
+      spyOn(nvmeofService, 'createListener').and.stub();
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.ts
new file mode 100644 (file)
index 0000000..bc02ea9
--- /dev/null
@@ -0,0 +1,115 @@
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ListenerRequest, NvmeofService } from '~/app/shared/api/nvmeof.service';
+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 { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { HostService } from '~/app/shared/api/host.service';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+@Component({
+  selector: 'cd-nvmeof-listeners-form',
+  templateUrl: './nvmeof-listeners-form.component.html',
+  styleUrls: ['./nvmeof-listeners-form.component.scss']
+})
+export class NvmeofListenersFormComponent implements OnInit {
+  action: string;
+  permission: Permission;
+  hostPermission: Permission;
+  resource: string;
+  pageURL: string;
+  listenerForm: CdFormGroup;
+  subsystemNQN: string;
+  hosts: Array<object> = null;
+
+  constructor(
+    public actionLabels: ActionLabelsI18n,
+    private authStorageService: AuthStorageService,
+    private taskWrapperService: TaskWrapperService,
+    private nvmeofService: NvmeofService,
+    private hostService: HostService,
+    private router: Router,
+    private route: ActivatedRoute,
+    public activeModal: NgbActiveModal,
+    public formatterService: FormatterService,
+    public dimlessBinaryPipe: DimlessBinaryPipe
+  ) {
+    this.permission = this.authStorageService.getPermissions().nvmeof;
+    this.hostPermission = this.authStorageService.getPermissions().hosts;
+    this.resource = $localize`Listener`;
+    this.pageURL = 'block/nvmeof/subsystems';
+  }
+
+  setHosts() {
+    const hostContext = new CdTableFetchDataContext(() => undefined);
+    this.hostService.list(hostContext.toParams(), 'false').subscribe((resp: any[]) => {
+      const nvmeofHosts = resp.filter((r) =>
+        r.service_instances.some((si: any) => si.type === 'nvmeof')
+      );
+      this.hosts = nvmeofHosts.map((h) => ({ hostname: h.hostname, addr: h.addr }));
+    });
+  }
+
+  ngOnInit() {
+    this.createForm();
+    this.action = this.actionLabels.CREATE;
+    this.route.params.subscribe((params: { subsystem_nqn: string }) => {
+      this.subsystemNQN = params.subsystem_nqn;
+    });
+    this.setHosts();
+  }
+
+  createForm() {
+    this.listenerForm = new CdFormGroup({
+      host: new UntypedFormControl(null, {
+        validators: [Validators.required]
+      }),
+      trsvcid: new UntypedFormControl(4420, [
+        Validators.required,
+        CdValidators.number(false),
+        Validators.max(65535)
+      ])
+    });
+  }
+
+  buildRequest(): ListenerRequest {
+    const host = this.listenerForm.getValue('host');
+    let trsvcid = Number(this.listenerForm.getValue('trsvcid'));
+    if (!trsvcid) trsvcid = 4420;
+    const request = {
+      host_name: host.hostname,
+      traddr: host.addr,
+      trsvcid
+    };
+    return request;
+  }
+
+  onSubmit() {
+    const component = this;
+    const taskUrl: string = `nvmeof/listener/${URLVerbs.CREATE}`;
+    const request = this.buildRequest();
+    this.taskWrapperService
+      .wrapTaskAroundCall({
+        task: new FinishedTask(taskUrl, {
+          nqn: this.subsystemNQN,
+          host_name: request.host_name
+        }),
+        call: this.nvmeofService.createListener(this.subsystemNQN, request)
+      })
+      .subscribe({
+        error() {
+          component.listenerForm.setErrors({ cdSubmitButton: true });
+        },
+        complete: () => {
+          this.router.navigate([this.pageURL, { outlets: { modal: null } }]);
+        }
+      });
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.html
new file mode 100644 (file)
index 0000000..da00155
--- /dev/null
@@ -0,0 +1,21 @@
+<legend>
+  <cd-help-text>
+    A listener defines the IP port on the gateway that is to process NVMe/TCP commands and I/O operations.
+  </cd-help-text>
+</legend>
+<cd-table [data]="listeners"
+          columnMode="flex"
+          (fetchData)="listListeners()"
+          [columns]="listenerColumns"
+          identifier="id"
+          forceIdentifier="true"
+          selectionType="single"
+          (updateSelection)="updateSelection($event)">
+  <div class="table-actions btn-toolbar">
+    <cd-table-actions [permission]="permission"
+                      [selection]="selection"
+                      class="btn-group"
+                      [tableActions]="tableActions">
+    </cd-table-actions>
+  </div>
+</cd-table>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.spec.ts
new file mode 100644 (file)
index 0000000..ecf8c49
--- /dev/null
@@ -0,0 +1,70 @@
+import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
+
+import { NvmeofListenersListComponent } from './nvmeof-listeners-list.component';
+import { HttpClientModule } from '@angular/common/http';
+import { RouterTestingModule } from '@angular/router/testing';
+import { SharedModule } from '~/app/shared/shared.module';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { of } from 'rxjs';
+
+const mockListeners = [
+  {
+    host_name: 'ceph-node-02',
+    trtype: 'TCP',
+    traddr: '192.168.100.102',
+    adrfam: 0,
+    trsvcid: 4421
+  }
+];
+
+class MockNvmeOfService {
+  listListeners() {
+    return of(mockListeners);
+  }
+}
+
+class MockAuthStorageService {
+  getPermissions() {
+    return { nvmeof: {} };
+  }
+}
+
+class MockModalService {}
+
+class MockTaskWrapperService {}
+
+describe('NvmeofListenersListComponent', () => {
+  let component: NvmeofListenersListComponent;
+  let fixture: ComponentFixture<NvmeofListenersListComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [NvmeofListenersListComponent],
+      imports: [HttpClientModule, RouterTestingModule, SharedModule],
+      providers: [
+        { provide: NvmeofService, useClass: MockNvmeOfService },
+        { provide: AuthStorageService, useClass: MockAuthStorageService },
+        { provide: ModalService, useClass: MockModalService },
+        { provide: TaskWrapperService, useClass: MockTaskWrapperService }
+      ]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(NvmeofListenersListComponent);
+    component = fixture.componentInstance;
+    component.ngOnInit();
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should retrieve subsystems', fakeAsync(() => {
+    component.listListeners();
+    tick();
+    expect(component.listeners).toEqual(mockListeners);
+  }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.ts
new file mode 100644 (file)
index 0000000..26b48d8
--- /dev/null
@@ -0,0 +1,123 @@
+import { Component, Input, OnChanges, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { NvmeofListener } from '~/app/shared/models/nvmeof';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+const BASE_URL = 'block/nvmeof/subsystems';
+
+@Component({
+  selector: 'cd-nvmeof-listeners-list',
+  templateUrl: './nvmeof-listeners-list.component.html',
+  styleUrls: ['./nvmeof-listeners-list.component.scss']
+})
+export class NvmeofListenersListComponent implements OnInit, OnChanges {
+  @Input()
+  subsystemNQN: string;
+
+  listenerColumns: any;
+  tableActions: CdTableAction[];
+  selection = new CdTableSelection();
+  permission: Permission;
+  listeners: NvmeofListener[];
+
+  constructor(
+    public actionLabels: ActionLabelsI18n,
+    private modalService: ModalService,
+    private authStorageService: AuthStorageService,
+    private taskWrapper: TaskWrapperService,
+    private nvmeofService: NvmeofService,
+    private router: Router
+  ) {
+    this.permission = this.authStorageService.getPermissions().nvmeof;
+  }
+
+  ngOnInit() {
+    this.listenerColumns = [
+      {
+        name: $localize`Host`,
+        prop: 'host_name'
+      },
+      {
+        name: $localize`Transport`,
+        prop: 'trtype'
+      },
+      {
+        name: $localize`Address`,
+        prop: 'full_addr',
+        cellTransformation: CellTemplate.copy
+      }
+    ];
+    this.tableActions = [
+      {
+        name: this.actionLabels.CREATE,
+        permission: 'create',
+        icon: Icons.add,
+        click: () =>
+          this.router.navigate([
+            BASE_URL,
+            { outlets: { modal: [URLVerbs.CREATE, this.subsystemNQN, 'listener'] } }
+          ]),
+        canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+      },
+      {
+        name: this.actionLabels.DELETE,
+        permission: 'delete',
+        icon: Icons.destroy,
+        click: () => this.deleteSubsystemModal()
+      }
+    ];
+  }
+
+  ngOnChanges() {
+    this.listListeners();
+  }
+
+  updateSelection(selection: CdTableSelection) {
+    this.selection = selection;
+  }
+
+  listListeners() {
+    this.nvmeofService
+      .listListeners(this.subsystemNQN)
+      .subscribe((listResponse: NvmeofListener[]) => {
+        this.listeners = listResponse.map((listener, index) => {
+          listener['id'] = index;
+          listener['full_addr'] = `${listener.traddr}:${listener.trsvcid}`;
+          return listener;
+        });
+      });
+  }
+
+  deleteSubsystemModal() {
+    const listener = this.selection.first();
+    this.modalService.show(CriticalConfirmationModalComponent, {
+      itemDescription: 'Listener',
+      actionDescription: 'delete',
+      itemNames: [`listener ${listener.host_name} (${listener.traddr}:${listener.trsvcid})`],
+      submitActionObservable: () =>
+        this.taskWrapper.wrapTaskAroundCall({
+          task: new FinishedTask('nvmeof/listener/delete', {
+            nqn: this.subsystemNQN,
+            host_name: listener.host_name
+          }),
+          call: this.nvmeofService.deleteListener(
+            this.subsystemNQN,
+            listener.host_name,
+            listener.traddr,
+            listener.trsvcid
+          )
+        })
+    });
+  }
+}
index 56a05dfecda59040755d70903e4e7a42c1c34c03..5b551bdb96611346ec36e8bf519f70ba741487a4 100644 (file)
         </cd-table-key-value>
       </ng-template>
     </ng-container>
+    <ng-container ngbNavItem="listeners">
+      <a ngbNavLink
+         i18n>Listeners</a>
+      <ng-template ngbNavContent>
+        <cd-nvmeof-listeners-list [subsystemNQN]="subsystemNQN">
+        </cd-nvmeof-listeners-list>
+      </ng-template>
+    </ng-container>
   </nav>
 
   <div [ngbNavOutlet]="nav"></div>
index a79b01d6704dd0216754d8c49e676802316f7f52..5e8abf9a4852f1475e1aa76e60def2f87d4cd686 100644 (file)
@@ -12,10 +12,12 @@ export class NvmeofSubsystemsDetailsComponent implements OnChanges {
 
   selectedItem: any;
   data: any;
+  subsystemNQN: string;
 
   ngOnChanges() {
     if (this.selection) {
       this.selectedItem = this.selection;
+      this.subsystemNQN = this.selectedItem.nqn;
       this.data = {};
       this.data[$localize`Serial Number`] = this.selectedItem.serial_number;
       this.data[$localize`Model Number`] = this.selectedItem.model_number;
index 8d5b8a3830c81b88fda3047d3220e7a325766dac..d9375ba0de3c39b7d26905b5570f67d4b35019b8 100644 (file)
@@ -5,6 +5,12 @@ import _ from 'lodash';
 import { Observable, of as observableOf } from 'rxjs';
 import { catchError, mapTo } from 'rxjs/operators';
 
+export interface ListenerRequest {
+  host_name: string;
+  traddr: string;
+  trsvcid: number;
+}
+
 const BASE_URL = 'api/nvmeof';
 
 @Injectable({
@@ -44,4 +50,27 @@ export class NvmeofService {
       })
     );
   }
+
+  // listeners
+  listListeners(subsystemNQN: string) {
+    return this.http.get(`${BASE_URL}/subsystem/${subsystemNQN}/listener`);
+  }
+
+  createListener(subsystemNQN: string, request: ListenerRequest) {
+    return this.http.post(`${BASE_URL}/subsystem/${subsystemNQN}/listener`, request, {
+      observe: 'response'
+    });
+  }
+
+  deleteListener(subsystemNQN: string, hostName: string, traddr: string, trsvcid: string) {
+    return this.http.delete(
+      `${BASE_URL}/subsystem/${subsystemNQN}/listener/${hostName}/${traddr}`,
+      {
+        observe: 'response',
+        params: {
+          trsvcid
+        }
+      }
+    );
+  }
 }
index a856a4c487019b0b5b3ee844d25d3a4905143488..e567981899f943ac2e235c2bc68fcb3421aa0ec9 100644 (file)
     </cd-copy-2-clipboard-button>
   </span>
 </ng-template>
+
+<ng-template #copyTpl
+             let-value="value">
+  <span class="font-monospace">{{value}}</span>
+  <cd-copy-2-clipboard-button *ngIf="value"
+                              [source]="value"
+                              [byId]="false"
+                              [showIconOnly]="true">
+  </cd-copy-2-clipboard-button>
+</ng-template>
index 905646b55b8a8efc56b817e6bd0ba9e62f7a801e..80588cc5dc85cafe5ae87d6a6928a88165f5424b 100644 (file)
@@ -77,6 +77,8 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   pathTpl: TemplateRef<any>;
   @ViewChild('tooltipTpl', { static: true })
   tooltipTpl: TemplateRef<any>;
+  @ViewChild('copyTpl', { static: true })
+  copyTpl: TemplateRef<any>;
 
   // This is the array with the items to be shown.
   @Input()
@@ -615,6 +617,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
     this.cellTemplates.timeAgo = this.timeAgoTpl;
     this.cellTemplates.path = this.pathTpl;
     this.cellTemplates.tooltip = this.tooltipTpl;
+    this.cellTemplates.copy = this.copyTpl;
   }
 
   useCustomClass(value: any): string {
index 5c4072f7f1fc6e9349c4d8b0a7e3d664bfc4f79c..bda66f6004e6a44731cad177af3d87ac84dff18b 100644 (file)
@@ -72,5 +72,12 @@ export enum CellTemplate {
   //     }
   //  }
   */
-  tooltip = 'tooltip'
+  tooltip = 'tooltip',
+  /*
+  This template is used to attach copy to clipboard functionality to the given column value
+  // {
+  //   ...
+  //   cellTransformation: CellTemplate.copy,
+  */
+  copy = 'copy'
 }
index e383d4a1dfca2cf4b6a07b34f9831fa83cd81acf..e86dae7dcd91af28ca704c5daad6b069b611641b 100644 (file)
@@ -19,3 +19,12 @@ export interface NvmeofSubsystem {
   subtype: string;
   max_namespaces: number;
 }
+
+export interface NvmeofListener {
+  host_name: string;
+  trtype: string;
+  traddr: string;
+  adrfam: number; // 0: IPv4, 1: IPv6
+  trsvcid: number; // 4420
+  id?: number; // for table
+}
index 3180738a09dfaf532bbffed8f686f6c91a3fc88a..75fa41dc5f790dd4be4d31488b02abbb0b4cdb14 100644 (file)
@@ -354,13 +354,20 @@ export class TaskMessageService {
     'iscsi/target/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
       this.iscsiTarget(metadata)
     ),
-    // NVME/TCP tasks
+    // nvmeof
     'nvmeof/subsystem/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
       this.nvmeofSubsystem(metadata)
     ),
     'nvmeof/subsystem/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
       this.nvmeofSubsystem(metadata)
     ),
+    'nvmeof/listener/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+      this.nvmeofListener(metadata)
+    ),
+    'nvmeof/listener/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
+      this.nvmeofListener(metadata)
+    ),
+    // nfs
     'nfs/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
       this.nfs(metadata)
     ),
@@ -490,6 +497,10 @@ export class TaskMessageService {
     return $localize`subsystem '${metadata.nqn}'`;
   }
 
+  nvmeofListener(metadata: any) {
+    return $localize`listener '${metadata.host_name} on subsystem ${metadata.nqn}`;
+  }
+
   nfs(metadata: any) {
     return $localize`NFS '${metadata.cluster_id}\:${
       metadata.export_id ? metadata.export_id : metadata.path
index 4d3f312eb485b883755bc42653523e3bf288fcd8..3f076d7b216ad069d502faded95259df9cb96ceb 100644 (file)
@@ -11,6 +11,7 @@ from ..services.orchestrator import OrchClient
 
 logger = logging.getLogger('nvmeof_conf')
 
+
 class NvmeofGatewayAlreadyExists(Exception):
     def __init__(self, gateway_name):
         super(NvmeofGatewayAlreadyExists, self).__init__(