it('should open and show breadcrumb', () => {
page.navigateTo();
- expect(Helper.getBreadcrumbText()).toEqual('iSCSI');
+ expect(Helper.getBreadcrumbText()).toEqual('Overview');
});
});
import { NgModule } from '@angular/core';
import { ActivatedRouteSnapshot, RouterModule, Routes } from '@angular/router';
+import { IscsiTargetListComponent } from './ceph/block/iscsi-target-list/iscsi-target-list.component';
import { IscsiComponent } from './ceph/block/iscsi/iscsi.component';
import { OverviewComponent as RbdMirroringComponent } from './ceph/block/mirroring/overview/overview.component';
import { RbdFormComponent } from './ceph/block/rbd-form/rbd-form.component';
component: RbdMirroringComponent,
data: { breadcrumbs: 'Mirroring' }
},
- { path: 'iscsi', component: IscsiComponent, data: { breadcrumbs: 'iSCSI' } }
+ // iSCSI
+ {
+ path: 'iscsi',
+ data: { breadcrumbs: 'iSCSI' },
+ children: [
+ {
+ path: '',
+ redirectTo: 'overview',
+ pathMatch: 'full'
+ },
+ {
+ path: 'overview',
+ data: { breadcrumbs: 'Overview' },
+ children: [{ path: '', component: IscsiComponent }]
+ },
+ {
+ path: 'targets',
+ data: { breadcrumbs: 'Targets' },
+ children: [
+ { path: '', component: IscsiTargetListComponent }
+ ]
+ }
+ ]
+ }
]
},
// Filesystems
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
+import { TreeModule } from 'ng2-tree';
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { ModalModule } from 'ngx-bootstrap/modal';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import { SharedModule } from '../../shared/shared.module';
+import { IscsiTabsComponent } from './iscsi-tabs/iscsi-tabs.component';
+import { IscsiTargetListComponent } from './iscsi-target-list/iscsi-target-list.component';
import { IscsiComponent } from './iscsi/iscsi.component';
import { MirroringModule } from './mirroring/mirroring.module';
import { RbdDetailsComponent } from './rbd-details/rbd-details.component';
import { RbdTrashMoveModalComponent } from './rbd-trash-move-modal/rbd-trash-move-modal.component';
import { RbdTrashPurgeModalComponent } from './rbd-trash-purge-modal/rbd-trash-purge-modal.component';
import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-trash-restore-modal.component';
+import { IscsiTargetDetailsComponent } from './iscsi-target-details/iscsi-target-details.component';
@NgModule({
entryComponents: [
RbdSnapshotFormComponent,
RbdTrashMoveModalComponent,
RbdTrashRestoreModalComponent,
- RbdTrashPurgeModalComponent
+ RbdTrashPurgeModalComponent,
+ IscsiTargetDetailsComponent
],
imports: [
CommonModule,
TooltipModule.forRoot(),
ModalModule.forRoot(),
SharedModule,
- RouterModule
+ RouterModule,
+ TreeModule
],
declarations: [
RbdListComponent,
IscsiComponent,
+ IscsiTabsComponent,
+ IscsiTargetListComponent,
RbdDetailsComponent,
RbdFormComponent,
RbdSnapshotListComponent,
RbdTrashMoveModalComponent,
RbdImagesComponent,
RbdTrashRestoreModalComponent,
- RbdTrashPurgeModalComponent
+ RbdTrashPurgeModalComponent,
+ IscsiTargetDetailsComponent
]
})
export class BlockModule {}
--- /dev/null
+<tabset>
+ <tab heading="Overview"
+ i18n-heading
+ [active]="url === '/block/iscsi/overview'"
+ (select)="navigateTo('/block/iscsi/overview')">
+ </tab>
+ <tab heading="Targets"
+ i18n-heading
+ [active]="url === '/block/iscsi/targets'"
+ (select)="navigateTo('/block/iscsi/targets')">
+ </tab>
+</tabset>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { configureTestBed } from '../../../../testing/unit-test-helper';
+import { SharedModule } from '../../../shared/shared.module';
+import { IscsiTabsComponent } from './iscsi-tabs.component';
+
+describe('IscsiTabsComponent', () => {
+ let component: IscsiTabsComponent;
+ let fixture: ComponentFixture<IscsiTabsComponent>;
+
+ configureTestBed({
+ imports: [SharedModule, TabsModule.forRoot(), RouterTestingModule],
+ declarations: [IscsiTabsComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IscsiTabsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+
+import { Router } from '@angular/router';
+
+@Component({
+ selector: 'cd-iscsi-tabs',
+ templateUrl: './iscsi-tabs.component.html',
+ styleUrls: ['./iscsi-tabs.component.scss']
+})
+export class IscsiTabsComponent implements OnInit {
+ url: string;
+
+ constructor(private router: Router) {}
+
+ ngOnInit() {
+ this.url = this.router.url;
+ }
+
+ navigateTo(url) {
+ this.router.navigate([url]);
+ }
+}
--- /dev/null
+<div class="col-sm-6 col-lg-6">
+ <legend i18n>iSCSI Topology</legend>
+ <tree [tree]="tree"
+ (nodeSelected)="onNodeSelected($event)">
+ <ng-template let-node>
+ <span class="node-name"
+ [innerHTML]="node.value"></span>
+ <span> </span>
+
+ <span class="label"
+ [ngClass]="{'label-success': ['in', 'up'].includes(node.status), 'label-danger': ['down', 'out'].includes(node.status)}">
+ {{ node.status }}
+ </span>
+ </ng-template>
+ </tree>
+</div>
+
+<div class="col-sm-6 col-lg-6 metadata"
+ *ngIf="data">
+ <legend>{{ title }}</legend>
+
+ <cd-table #detailTable
+ [data]="data"
+ columnMode="flex"
+ [columns]="columns"
+ [limit]="0">
+ </cd-table>
+</div>
+
+<ng-template #highlightTpl
+ let-row="row"
+ let-value="value">
+ <span *ngIf="row.default === undefined || row.default === row.current">{{ value }}</span>
+ <strong *ngIf="row.default !== undefined && row.default !== row.current">{{ value }}</strong>
+</ng-template>
--- /dev/null
+::ng-deep tree .fa {
+ font-weight: unset !important;
+}
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NodeEvent, Tree, TreeModule } from 'ng2-tree';
+
+import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { SharedModule } from '../../../shared/shared.module';
+import { IscsiTargetDetailsComponent } from './iscsi-target-details.component';
+
+describe('IscsiTargetDetailsComponent', () => {
+ let component: IscsiTargetDetailsComponent;
+ let fixture: ComponentFixture<IscsiTargetDetailsComponent>;
+
+ configureTestBed({
+ declarations: [IscsiTargetDetailsComponent],
+ imports: [TreeModule, SharedModule],
+ providers: [i18nProviders]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IscsiTargetDetailsComponent);
+ component = fixture.componentInstance;
+
+ component.settings = {
+ config: { minimum_gateways: 2 },
+ disk_default_controls: {
+ hw_max_sectors: 1024,
+ max_data_area_mb: 8
+ },
+ target_default_controls: {
+ cmdsn_depth: 128,
+ dataout_timeout: 20
+ }
+ };
+ component.selection = new CdTableSelection();
+ component.selection.selected = [
+ {
+ target_iqn: 'iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw',
+ portals: [{ host: 'node1', ip: '192.168.100.201' }],
+ disks: [{ pool: 'rbd', image: 'disk_1', controls: { hw_max_sectors: 1 } }],
+ clients: [
+ {
+ client_iqn: 'iqn.1994-05.com.redhat:rh7-client',
+ luns: [{ pool: 'rbd', image: 'disk_1' }],
+ auth: {
+ user: 'myiscsiusername'
+ }
+ }
+ ],
+ groups: [],
+ target_controls: { dataout_timeout: 2 }
+ }
+ ];
+ component.selection.update();
+
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should empty data and generateTree when ngOnChanges is called', () => {
+ const tempData = [{ current: 'baz', default: 'bar', displayName: 'foo' }];
+ component.data = tempData;
+ fixture.detectChanges();
+
+ expect(component.data).toEqual(tempData);
+ expect(component.metadata).toEqual({});
+ expect(component.tree).toEqual(undefined);
+
+ component.ngOnChanges();
+
+ expect(component.data).toBeUndefined();
+ expect(component.metadata).toEqual({
+ 'client_iqn.1994-05.com.redhat:rh7-client': { user: 'myiscsiusername' },
+ disk_rbd_disk_1: { hw_max_sectors: 1 },
+ root: { dataout_timeout: 2 }
+ });
+ expect(component.tree).toEqual({
+ children: [
+ {
+ children: [{ id: 'disk_rbd_disk_1', value: 'rbd/disk_1' }],
+ settings: {
+ cssClasses: { expanded: 'fa fa-fw fa-hdd-o fa-lg', leaf: 'fa fa-fw fa-hdd-o' },
+ selectionAllowed: false
+ },
+ value: 'Disks'
+ },
+ {
+ children: [{ value: 'node1:192.168.100.201' }],
+ settings: {
+ cssClasses: { expanded: 'fa fa-fw fa-server fa-lg', leaf: 'fa fa-fw fa-server fa-lg' },
+ selectionAllowed: false
+ },
+ value: 'Portals'
+ },
+ {
+ children: [
+ {
+ children: [
+ {
+ id: 'disk_rbd_disk_1',
+ settings: {
+ cssClasses: { expanded: 'fa fa-fw fa-hdd-o fa-lg', leaf: 'fa fa-fw fa-hdd-o' }
+ },
+ value: 'rbd/disk_1'
+ }
+ ],
+ id: 'client_iqn.1994-05.com.redhat:rh7-client',
+ value: 'iqn.1994-05.com.redhat:rh7-client'
+ }
+ ],
+ settings: {
+ cssClasses: { expanded: 'fa fa-fw fa-user fa-lg', leaf: 'fa fa-fw fa-user' },
+ selectionAllowed: false
+ },
+ value: 'Initiators'
+ },
+ {
+ children: [],
+ settings: {
+ cssClasses: { expanded: 'fa fa-fw fa-users fa-lg', leaf: 'fa fa-fw fa-users' },
+ selectionAllowed: false
+ },
+ value: 'Groups'
+ }
+ ],
+ id: 'root',
+ settings: { cssClasses: { expanded: 'fa fa-fw fa-bullseye fa-lg' }, static: true },
+ value: 'iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw'
+ });
+ });
+
+ describe('should update data when onNodeSelected is called', () => {
+ beforeEach(() => {
+ component.ngOnChanges();
+ });
+
+ it('with target selected', () => {
+ const tree = new Tree(component.tree);
+ const node = new NodeEvent(tree);
+ component.onNodeSelected(node);
+ expect(component.data).toEqual([
+ { current: 128, default: 128, displayName: 'cmdsn_depth' },
+ { current: 2, default: 20, displayName: 'dataout_timeout' }
+ ]);
+ });
+
+ it('with disk selected', () => {
+ const tree = new Tree(component.tree.children[0].children[0]);
+ const node = new NodeEvent(tree);
+ component.onNodeSelected(node);
+ expect(component.data).toEqual([
+ { current: 1, default: 1024, displayName: 'hw_max_sectors' },
+ { current: 8, default: 8, displayName: 'max_data_area_mb' }
+ ]);
+ });
+
+ it('with initiator selected', () => {
+ const tree = new Tree(component.tree.children[2].children[0]);
+ const node = new NodeEvent(tree);
+ component.onNodeSelected(node);
+ expect(component.data).toEqual([
+ { current: 'myiscsiusername', default: undefined, displayName: 'user' }
+ ]);
+ });
+
+ it('with any other selected', () => {
+ const tree = new Tree(component.tree.children[1].children[0]);
+ const node = new NodeEvent(tree);
+ component.onNodeSelected(node);
+ expect(component.data).toBeUndefined();
+ });
+ });
+});
--- /dev/null
+import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { I18n } from '@ngx-translate/i18n-polyfill';
+import * as _ from 'lodash';
+import { NodeEvent, TreeModel } from 'ng2-tree';
+
+import { TableComponent } from '../../../shared/datatable/table/table.component';
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+
+@Component({
+ selector: 'cd-iscsi-target-details',
+ templateUrl: './iscsi-target-details.component.html',
+ styleUrls: ['./iscsi-target-details.component.scss']
+})
+export class IscsiTargetDetailsComponent implements OnChanges, OnInit {
+ @Input()
+ selection: CdTableSelection;
+ @Input()
+ settings: any;
+
+ @ViewChild('highlightTpl')
+ highlightTpl: TemplateRef<any>;
+
+ private detailTable: TableComponent;
+ @ViewChild('detailTable')
+ set content(content: TableComponent) {
+ this.detailTable = content;
+ if (content) {
+ content.updateColumns();
+ }
+ }
+
+ columns: CdTableColumn[];
+ data: any;
+ metadata: any = {};
+ selectedItem: any;
+ title: string;
+ tree: TreeModel;
+
+ constructor(private i18n: I18n) {}
+
+ ngOnInit() {
+ this.columns = [
+ {
+ prop: 'displayName',
+ name: this.i18n('Name'),
+ flexGrow: 2,
+ cellTemplate: this.highlightTpl
+ },
+ {
+ prop: 'current',
+ name: this.i18n('Current'),
+ flexGrow: 1,
+ cellTemplate: this.highlightTpl
+ },
+ {
+ prop: 'default',
+ name: this.i18n('Default'),
+ flexGrow: 1,
+ cellTemplate: this.highlightTpl
+ }
+ ];
+ }
+
+ ngOnChanges() {
+ if (this.selection.hasSelection) {
+ this.selectedItem = this.selection.first();
+ this.generateTree();
+ }
+
+ this.data = undefined;
+ }
+
+ private generateTree() {
+ this.metadata = { root: this.selectedItem.target_controls };
+
+ const cssClasses = {
+ target: {
+ expanded: 'fa fa-fw fa-bullseye fa-lg'
+ },
+ initiators: {
+ expanded: 'fa fa-fw fa-user fa-lg',
+ leaf: 'fa fa-fw fa-user'
+ },
+ groups: {
+ expanded: 'fa fa-fw fa-users fa-lg',
+ leaf: 'fa fa-fw fa-users'
+ },
+ disks: {
+ expanded: 'fa fa-fw fa-hdd-o fa-lg',
+ leaf: 'fa fa-fw fa-hdd-o'
+ },
+ portals: {
+ expanded: 'fa fa-fw fa-server fa-lg',
+ leaf: 'fa fa-fw fa-server fa-lg'
+ }
+ };
+
+ const disks = [];
+ _.forEach(this.selectedItem.disks, (disk) => {
+ const id = 'disk_' + disk.pool + '_' + disk.image;
+ this.metadata[id] = disk.controls;
+ disks.push({
+ value: `${disk.pool}/${disk.image}`,
+ id: id
+ });
+ });
+
+ const portals = [];
+ _.forEach(this.selectedItem.portals, (portal) => {
+ portals.push({ value: `${portal.host}:${portal.ip}` });
+ });
+
+ const clients = [];
+ _.forEach(this.selectedItem.clients, (client) => {
+ this.metadata['client_' + client.client_iqn] = client.auth;
+
+ const luns = [];
+ client.luns.forEach((lun) => {
+ luns.push({
+ value: `${lun.pool}/${lun.image}`,
+ id: 'disk_' + lun.pool + '_' + lun.image,
+ settings: {
+ cssClasses: cssClasses.disks
+ }
+ });
+ });
+
+ clients.push({
+ value: client.client_iqn,
+ id: 'client_' + client.client_iqn,
+ children: luns
+ });
+ });
+
+ const groups = [];
+ _.forEach(this.selectedItem.groups, (group) => {
+ const luns = [];
+ group.disks.forEach((disk) => {
+ luns.push({
+ value: `${disk.pool}/${disk.image}`,
+ id: 'disk_' + disk.pool + '_' + disk.image
+ });
+ });
+
+ const initiators = [];
+ group.members.forEach((member) => {
+ initiators.push({
+ value: member,
+ id: 'client_' + member
+ });
+ });
+
+ groups.push({
+ value: group.group_id,
+ children: [
+ {
+ value: 'Disks',
+ children: luns,
+ settings: {
+ selectionAllowed: false,
+ cssClasses: cssClasses.disks
+ }
+ },
+ {
+ value: 'Initiators',
+ children: initiators,
+ settings: {
+ selectionAllowed: false,
+ cssClasses: cssClasses.initiators
+ }
+ }
+ ]
+ });
+ });
+
+ this.tree = {
+ value: this.selectedItem.target_iqn,
+ id: 'root',
+ settings: {
+ static: true,
+ cssClasses: cssClasses.target
+ },
+ children: [
+ {
+ value: 'Disks',
+ children: disks,
+ settings: {
+ selectionAllowed: false,
+ cssClasses: cssClasses.disks
+ }
+ },
+ {
+ value: 'Portals',
+ children: portals,
+ settings: {
+ selectionAllowed: false,
+ cssClasses: cssClasses.portals
+ }
+ },
+ {
+ value: 'Initiators',
+ children: clients,
+ settings: {
+ selectionAllowed: false,
+ cssClasses: cssClasses.initiators
+ }
+ },
+ {
+ value: 'Groups',
+ children: groups,
+ settings: {
+ selectionAllowed: false,
+ cssClasses: cssClasses.groups
+ }
+ }
+ ]
+ };
+ }
+
+ onNodeSelected(e: NodeEvent) {
+ if (e.node.id) {
+ this.title = e.node.value;
+ const tempData = this.metadata[e.node.id] || {};
+
+ if (e.node.id === 'root') {
+ this.columns[2].isHidden = false;
+ this.data = _.map(this.settings.target_default_controls, (value, key) => {
+ return {
+ displayName: key,
+ default: value,
+ current: tempData[key] || value
+ };
+ });
+ } else if (e.node.id.toString().startsWith('disk_')) {
+ this.columns[2].isHidden = false;
+ this.data = _.map(this.settings.disk_default_controls, (value, key) => {
+ return {
+ displayName: key,
+ default: value,
+ current: tempData[key] || value
+ };
+ });
+ } else {
+ this.columns[2].isHidden = true;
+ this.data = _.map(tempData, (value, key) => {
+ return {
+ displayName: key,
+ default: undefined,
+ current: value
+ };
+ });
+ }
+ } else {
+ this.data = undefined;
+ }
+
+ if (this.detailTable) {
+ this.detailTable.updateColumns();
+ }
+ }
+}
--- /dev/null
+<cd-iscsi-tabs></cd-iscsi-tabs>
+
+<cd-info-panel *ngIf="available === false"
+ title="iSCSI Targets not available"
+ i18n-title>
+ <ng-container i18n>Please consult the <a href="{{docsUrl}}"
+ target="_blank">documentation</a>
+ on how to configure and enable the iSCSI Targets management functionality.</ng-container>
+
+ <ng-container *ngIf="status">
+ <br>
+ <span i18n>Available information:</span>
+ <pre>{{ status }}</pre>
+ </ng-container>
+</cd-info-panel>
+
+<cd-table #table
+ *ngIf="available === true"
+ [data]="targets"
+ columnMode="flex"
+ [columns]="columns"
+ identifier="target_iqn"
+ forceIdentifier="true"
+ selectionType="single"
+ (updateSelection)="updateSelection($event)">
+ <cd-iscsi-target-details cdTableDetail
+ *ngIf="selection.hasSingleSelection"
+ [selection]="selection"
+ [settings]="settings"></cd-iscsi-target-details>
+</cd-table>
--- /dev/null
+::ng-deep tabset.tabset > ul {
+ border-bottom: 1px solid #ddd;
+ float: left;
+ display: block;
+ margin-right: 20px;
+ border-bottom: 0;
+ border-right: 1px solid #ddd;
+ padding-right: 15px;
+}
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastModule } from 'ng2-toastr';
+import { TreeModule } from 'ng2-tree';
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
+import { TaskListService } from '../../../shared/services/task-list.service';
+import { SharedModule } from '../../../shared/shared.module';
+import { IscsiTabsComponent } from '../iscsi-tabs/iscsi-tabs.component';
+import { IscsiTargetDetailsComponent } from '../iscsi-target-details/iscsi-target-details.component';
+import { IscsiTargetListComponent } from './iscsi-target-list.component';
+
+describe('IscsiTargetListComponent', () => {
+ let component: IscsiTargetListComponent;
+ let fixture: ComponentFixture<IscsiTargetListComponent>;
+
+ configureTestBed({
+ imports: [
+ HttpClientTestingModule,
+ RouterTestingModule,
+ SharedModule,
+ TabsModule.forRoot(),
+ TreeModule,
+ ToastModule.forRoot()
+ ],
+ declarations: [IscsiTargetListComponent, IscsiTabsComponent, IscsiTargetDetailsComponent],
+ providers: [TaskListService, i18nProviders]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IscsiTargetListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
+
+import { I18n } from '@ngx-translate/i18n-polyfill';
+import { Subscription } from 'rxjs';
+
+import { IscsiService } from '../../../shared/api/iscsi.service';
+import { TableComponent } from '../../../shared/datatable/table/table.component';
+import { CellTemplate } from '../../../shared/enum/cell-template.enum';
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { Permissions } from '../../../shared/models/permissions';
+import { CephReleaseNamePipe } from '../../../shared/pipes/ceph-release-name.pipe';
+import { AuthStorageService } from '../../../shared/services/auth-storage.service';
+import { SummaryService } from '../../../shared/services/summary.service';
+import { TaskListService } from '../../../shared/services/task-list.service';
+
+@Component({
+ selector: 'cd-iscsi-target-list',
+ templateUrl: './iscsi-target-list.component.html',
+ styleUrls: ['./iscsi-target-list.component.scss'],
+ providers: [TaskListService]
+})
+export class IscsiTargetListComponent implements OnInit, OnDestroy {
+ @ViewChild(TableComponent)
+ table: TableComponent;
+
+ available: boolean = undefined;
+ columns: CdTableColumn[];
+ docsUrl: string;
+ modalRef: BsModalRef;
+ permissions: Permissions;
+ selection = new CdTableSelection();
+ settings: any;
+ status: string;
+ summaryDataSubscription: Subscription;
+ tableActions: CdTableAction[];
+ targets = [];
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private i18n: I18n,
+ private iscsiService: IscsiService,
+ private taskListService: TaskListService,
+ private cephReleaseNamePipe: CephReleaseNamePipe,
+ private summaryservice: SummaryService
+ ) {
+ this.permissions = this.authStorageService.getPermissions();
+ }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: this.i18n('Target'),
+ prop: 'target_iqn',
+ flexGrow: 2,
+ cellTransformation: CellTemplate.executing
+ },
+ {
+ name: this.i18n('Portals'),
+ prop: 'cdPortals',
+ flexGrow: 2
+ },
+ {
+ name: this.i18n('Images'),
+ prop: 'cdImages',
+ flexGrow: 2
+ }
+ ];
+
+ this.iscsiService.status().subscribe((result: any) => {
+ this.available = result.available;
+
+ if (result.available) {
+ this.taskListService.init(
+ () => this.iscsiService.listTargets(),
+ (resp) => this.prepareResponse(resp),
+ (targets) => (this.targets = targets),
+ () => this.onFetchError(),
+ () => false,
+ () => false,
+ undefined
+ );
+
+ this.iscsiService.settings().subscribe((settings: any) => {
+ this.settings = settings;
+ });
+ } else {
+ const summary = this.summaryservice.getCurrentSummary();
+ const releaseName = this.cephReleaseNamePipe.transform(summary.version);
+ this.docsUrl = `http://docs.ceph.com/docs/${releaseName}/rbd/iscsi-targets/`;
+ this.status = result.message;
+ }
+ });
+ }
+
+ ngOnDestroy() {
+ if (this.summaryDataSubscription) {
+ this.summaryDataSubscription.unsubscribe();
+ }
+ }
+
+ prepareResponse(resp: any): any[] {
+ resp.forEach((element) => {
+ element.cdPortals = element.portals.map((portal) => `${portal.host}:${portal.ip}`);
+ element.cdImages = element.disks.map((disk) => `${disk.pool}/${disk.image}`);
+ });
+
+ return resp;
+ }
+
+ onFetchError() {
+ this.table.reset(); // Disable loading indicator.
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+}
+<cd-iscsi-tabs></cd-iscsi-tabs>
+
<legend i18n>Daemons</legend>
<cd-table [data]="daemons"
(fetchData)="refresh()"
--- /dev/null
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '../../../testing/unit-test-helper';
+import { UserFormModel } from '../../core/auth/user-form/user-form.model';
+import { UserService } from './user.service';
+
+describe('UserService', () => {
+ let service: UserService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [UserService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.get(UserService);
+ httpTesting = TestBed.get(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call create', () => {
+ const user = new UserFormModel();
+ user.username = 'user0';
+ user.password = 'pass0';
+ user.name = 'User 0';
+ user.email = 'user0@email.com';
+ user.roles = ['administrator'];
+ service.create(user).subscribe();
+ const req = httpTesting.expectOne('api/user');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual(user);
+ });
+
+ it('should call delete', () => {
+ service.delete('user0').subscribe();
+ const req = httpTesting.expectOne('api/user/user0');
+ expect(req.request.method).toBe('DELETE');
+ });
+
+ it('should call update', () => {
+ const user = new UserFormModel();
+ user.username = 'user0';
+ user.password = 'pass0';
+ user.name = 'User 0';
+ user.email = 'user0@email.com';
+ user.roles = ['administrator'];
+ service.update(user).subscribe();
+ const req = httpTesting.expectOne('api/user/user0');
+ expect(req.request.body).toEqual(user);
+ expect(req.request.method).toBe('PUT');
+ });
+
+ it('should call get', () => {
+ service.get('user0').subscribe();
+ const req = httpTesting.expectOne('api/user/user0');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call list', () => {
+ service.list().subscribe();
+ const req = httpTesting.expectOne('api/user');
+ expect(req.request.method).toBe('GET');
+ });
+});
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { ApiModule } from './api.module';
+
+@Injectable({
+ providedIn: ApiModule
+})
+export class IscsiService {
+ constructor(private http: HttpClient) {}
+
+ targetAdvancedSettings = {
+ cmdsn_depth: {
+ help: ''
+ },
+ dataout_timeout: {
+ help: ''
+ },
+ first_burst_length: {
+ help: ''
+ },
+ immediate_data: {
+ help: ''
+ },
+ initial_r2t: {
+ help: ''
+ },
+ max_burst_length: {
+ help: ''
+ },
+ max_outstanding_r2t: {
+ help: ''
+ },
+ max_recv_data_segment_length: {
+ help: ''
+ },
+ max_xmit_data_segment_length: {
+ help: ''
+ },
+ nopin_response_timeout: {
+ help: ''
+ },
+ nopin_timeout: {
+ help: ''
+ }
+ };
+
+ imageAdvancedSettings = {
+ hw_max_sectors: {
+ help: ''
+ },
+ max_data_area_mb: {
+ help: ''
+ },
+ osd_op_timeout: {
+ help: ''
+ },
+ qfull_timeout: {
+ help: ''
+ }
+ };
+
+ listTargets() {
+ return this.http.get(`api/iscsi/target`);
+ }
+
+ status() {
+ return this.http.get(`ui-api/iscsi/status`);
+ }
+
+ settings() {
+ return this.http.get(`ui-api/iscsi/settings`);
+ }
+
+ portals() {
+ return this.http.get(`ui-api/iscsi/portals`);
+ }
+
+ createTarget(target) {
+ return this.http.post(`api/iscsi/target`, target, { observe: 'response' });
+ }
+
+ deleteTarget(targetIqn) {
+ return this.http.delete(`api/iscsi/target/${targetIqn}`, { observe: 'response' });
+ }
+}