]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.ts
b989c9c1921bd0076d6bf33ade1f91eccdcbe84d
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / block / iscsi-target-details / iscsi-target-details.component.ts
1 import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
2
3 import { I18n } from '@ngx-translate/i18n-polyfill';
4 import {
5 ITreeOptions,
6 TREE_ACTIONS,
7 TreeComponent,
8 TreeModel,
9 TreeNode
10 } from 'angular-tree-component';
11 import * as _ from 'lodash';
12
13 import { TableComponent } from '../../../shared/datatable/table/table.component';
14 import { Icons } from '../../../shared/enum/icons.enum';
15 import { CdTableColumn } from '../../../shared/models/cd-table-column';
16 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
17 import { BooleanTextPipe } from '../../../shared/pipes/boolean-text.pipe';
18 import { IscsiBackstorePipe } from '../../../shared/pipes/iscsi-backstore.pipe';
19
20 @Component({
21 selector: 'cd-iscsi-target-details',
22 templateUrl: './iscsi-target-details.component.html',
23 styleUrls: ['./iscsi-target-details.component.scss']
24 })
25 export class IscsiTargetDetailsComponent implements OnChanges, OnInit {
26 @Input()
27 selection: CdTableSelection;
28 @Input()
29 settings: any;
30 @Input()
31 cephIscsiConfigVersion: number;
32
33 @ViewChild('highlightTpl', { static: true })
34 highlightTpl: TemplateRef<any>;
35
36 private detailTable: TableComponent;
37 @ViewChild('detailTable', { static: false })
38 set content(content: TableComponent) {
39 this.detailTable = content;
40 if (content) {
41 content.updateColumns();
42 }
43 }
44
45 @ViewChild('tree', { static: false }) tree: TreeComponent;
46
47 icons = Icons;
48 columns: CdTableColumn[];
49 data: any;
50 metadata: any = {};
51 selectedItem: any;
52 title: string;
53
54 nodes: any[] = [];
55 treeOptions: ITreeOptions = {
56 useVirtualScroll: true,
57 actionMapping: {
58 mouse: {
59 click: this.onNodeSelected.bind(this)
60 }
61 }
62 };
63
64 constructor(
65 private i18n: I18n,
66 private iscsiBackstorePipe: IscsiBackstorePipe,
67 private booleanTextPipe: BooleanTextPipe
68 ) {}
69
70 ngOnInit() {
71 this.columns = [
72 {
73 prop: 'displayName',
74 name: this.i18n('Name'),
75 flexGrow: 1,
76 cellTemplate: this.highlightTpl
77 },
78 {
79 prop: 'current',
80 name: this.i18n('Current'),
81 flexGrow: 1,
82 cellTemplate: this.highlightTpl
83 },
84 {
85 prop: 'default',
86 name: this.i18n('Default'),
87 flexGrow: 1,
88 cellTemplate: this.highlightTpl
89 }
90 ];
91 }
92
93 ngOnChanges() {
94 if (this.selection.hasSelection) {
95 this.selectedItem = this.selection.first();
96 this.generateTree();
97 }
98
99 this.data = undefined;
100 }
101
102 private generateTree() {
103 const target_meta = _.cloneDeep(this.selectedItem.target_controls);
104 // Target level authentication was introduced in ceph-iscsi config v11
105 if (this.cephIscsiConfigVersion > 10) {
106 _.extend(target_meta, _.cloneDeep(this.selectedItem.auth));
107 }
108 this.metadata = { root: target_meta };
109 const cssClasses = {
110 target: {
111 expanded: _.join(
112 this.selectedItem.cdExecuting
113 ? [Icons.large, Icons.spinner, Icons.spin]
114 : [Icons.large, Icons.bullseye],
115 ' '
116 )
117 },
118 initiators: {
119 expanded: _.join([Icons.large, Icons.user], ' '),
120 leaf: _.join([Icons.user], ' ')
121 },
122 groups: {
123 expanded: _.join([Icons.large, Icons.users], ' '),
124 leaf: _.join([Icons.users], ' ')
125 },
126 disks: {
127 expanded: _.join([Icons.large, Icons.disk], ' '),
128 leaf: _.join([Icons.disk], ' ')
129 },
130 portals: {
131 expanded: _.join([Icons.large, Icons.server], ' '),
132 leaf: _.join([Icons.server], ' ')
133 }
134 };
135
136 const disks: any[] = [];
137 _.forEach(this.selectedItem.disks, (disk) => {
138 const cdId = 'disk_' + disk.pool + '_' + disk.image;
139 this.metadata[cdId] = {
140 controls: disk.controls,
141 backstore: disk.backstore
142 };
143 ['wwn', 'lun'].forEach((k) => {
144 if (k in disk) {
145 this.metadata[cdId][k] = disk[k];
146 }
147 });
148 disks.push({
149 name: `${disk.pool}/${disk.image}`,
150 cdId: cdId,
151 cdIcon: cssClasses.disks.leaf
152 });
153 });
154
155 const portals: any[] = [];
156 _.forEach(this.selectedItem.portals, (portal) => {
157 portals.push({
158 name: `${portal.host}:${portal.ip}`,
159 cdIcon: cssClasses.portals.leaf
160 });
161 });
162
163 const clients: any[] = [];
164 _.forEach(this.selectedItem.clients, (client) => {
165 const client_metadata = _.cloneDeep(client.auth);
166 if (client.info) {
167 _.extend(client_metadata, client.info);
168 delete client_metadata['state'];
169 _.forEach(Object.keys(client.info.state), (state) => {
170 client_metadata[state.toLowerCase()] = client.info.state[state];
171 });
172 }
173 this.metadata['client_' + client.client_iqn] = client_metadata;
174
175 const luns: any[] = [];
176 client.luns.forEach((lun: Record<string, any>) => {
177 luns.push({
178 name: `${lun.pool}/${lun.image}`,
179 cdId: 'disk_' + lun.pool + '_' + lun.image,
180 cdIcon: cssClasses.disks.leaf
181 });
182 });
183
184 let status = '';
185 if (client.info) {
186 status = Object.keys(client.info.state).includes('LOGGED_IN') ? 'logged_in' : 'logged_out';
187 }
188 clients.push({
189 name: client.client_iqn,
190 status: status,
191 cdId: 'client_' + client.client_iqn,
192 children: luns,
193 cdIcon: cssClasses.initiators.leaf
194 });
195 });
196
197 const groups: any[] = [];
198 _.forEach(this.selectedItem.groups, (group) => {
199 const luns: any[] = [];
200 group.disks.forEach((disk: Record<string, any>) => {
201 luns.push({
202 name: `${disk.pool}/${disk.image}`,
203 cdId: 'disk_' + disk.pool + '_' + disk.image,
204 cdIcon: cssClasses.disks.leaf
205 });
206 });
207
208 const initiators: any[] = [];
209 group.members.forEach((member: string) => {
210 initiators.push({
211 name: member,
212 cdId: 'client_' + member
213 });
214 });
215
216 groups.push({
217 name: group.group_id,
218 cdIcon: cssClasses.groups.leaf,
219 children: [
220 {
221 name: 'Disks',
222 children: luns,
223 cdIcon: cssClasses.disks.expanded
224 },
225 {
226 name: 'Initiators',
227 children: initiators,
228 cdIcon: cssClasses.initiators.expanded
229 }
230 ]
231 });
232 });
233
234 this.nodes = [
235 {
236 name: this.selectedItem.target_iqn,
237 cdId: 'root',
238 isExpanded: true,
239 cdIcon: cssClasses.target.expanded,
240 children: [
241 {
242 name: 'Disks',
243 isExpanded: true,
244 children: disks,
245 cdIcon: cssClasses.disks.expanded
246 },
247 {
248 name: 'Portals',
249 isExpanded: true,
250 children: portals,
251 cdIcon: cssClasses.portals.expanded
252 },
253 {
254 name: 'Initiators',
255 isExpanded: true,
256 children: clients,
257 cdIcon: cssClasses.initiators.expanded
258 },
259 {
260 name: 'Groups',
261 isExpanded: true,
262 children: groups,
263 cdIcon: cssClasses.groups.expanded
264 }
265 ]
266 }
267 ];
268 }
269
270 private format(value: any) {
271 if (typeof value === 'boolean') {
272 return this.booleanTextPipe.transform(value);
273 }
274 return value;
275 }
276
277 onNodeSelected(tree: TreeModel, node: TreeNode) {
278 TREE_ACTIONS.ACTIVATE(tree, node, true);
279 if (node.data.cdId) {
280 this.title = node.data.name;
281 const tempData = this.metadata[node.data.cdId] || {};
282
283 if (node.data.cdId === 'root') {
284 this.columns[2].isHidden = false;
285 this.data = _.map(this.settings.target_default_controls, (value, key) => {
286 value = this.format(value);
287 return {
288 displayName: key,
289 default: value,
290 current: !_.isUndefined(tempData[key]) ? this.format(tempData[key]) : value
291 };
292 });
293 // Target level authentication was introduced in ceph-iscsi config v11
294 if (this.cephIscsiConfigVersion > 10) {
295 ['user', 'password', 'mutual_user', 'mutual_password'].forEach((key) => {
296 this.data.push({
297 displayName: key,
298 default: null,
299 current: tempData[key]
300 });
301 });
302 }
303 } else if (node.data.cdId.toString().startsWith('disk_')) {
304 this.columns[2].isHidden = false;
305 this.data = _.map(this.settings.disk_default_controls[tempData.backstore], (value, key) => {
306 value = this.format(value);
307 return {
308 displayName: key,
309 default: value,
310 current: !_.isUndefined(tempData.controls[key])
311 ? this.format(tempData.controls[key])
312 : value
313 };
314 });
315 this.data.push({
316 displayName: 'backstore',
317 default: this.iscsiBackstorePipe.transform(this.settings.default_backstore),
318 current: this.iscsiBackstorePipe.transform(tempData.backstore)
319 });
320 ['wwn', 'lun'].forEach((k) => {
321 if (k in tempData) {
322 this.data.push({
323 displayName: k,
324 default: undefined,
325 current: tempData[k]
326 });
327 }
328 });
329 } else {
330 this.columns[2].isHidden = true;
331 this.data = _.map(tempData, (value, key) => {
332 return {
333 displayName: key,
334 default: undefined,
335 current: this.format(value)
336 };
337 });
338 }
339 } else {
340 this.data = undefined;
341 }
342
343 if (this.detailTable) {
344 this.detailTable.updateColumns();
345 }
346 }
347
348 onUpdateData() {
349 this.tree.treeModel.expandAll();
350 }
351 }