]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts
import ceph 16.2.7
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / cluster / hosts / hosts.component.ts
1 import { Component, Input, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
2 import { Router } from '@angular/router';
3
4 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
5 import _ from 'lodash';
6 import { Subscription } from 'rxjs';
7 import { map, mergeMap } from 'rxjs/operators';
8
9 import { HostService } from '~/app/shared/api/host.service';
10 import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
11 import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
12 import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
13 import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
14 import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
15 import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
16 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
17 import { TableComponent } from '~/app/shared/datatable/table/table.component';
18 import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
19 import { Icons } from '~/app/shared/enum/icons.enum';
20 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
21 import { CdTableAction } from '~/app/shared/models/cd-table-action';
22 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
23 import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
24 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
25 import { FinishedTask } from '~/app/shared/models/finished-task';
26 import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum';
27 import { OrchestratorStatus } from '~/app/shared/models/orchestrator.interface';
28 import { Permissions } from '~/app/shared/models/permissions';
29 import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
30 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
31 import { ModalService } from '~/app/shared/services/modal.service';
32 import { NotificationService } from '~/app/shared/services/notification.service';
33 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
34 import { URLBuilderService } from '~/app/shared/services/url-builder.service';
35 import { HostFormComponent } from './host-form/host-form.component';
36
37 const BASE_URL = 'hosts';
38
39 @Component({
40 selector: 'cd-hosts',
41 templateUrl: './hosts.component.html',
42 styleUrls: ['./hosts.component.scss'],
43 providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
44 })
45 export class HostsComponent extends ListWithDetails implements OnDestroy, OnInit {
46 private sub = new Subscription();
47
48 @ViewChild(TableComponent)
49 table: TableComponent;
50 @ViewChild('servicesTpl', { static: true })
51 public servicesTpl: TemplateRef<any>;
52 @ViewChild('maintenanceConfirmTpl', { static: true })
53 maintenanceConfirmTpl: TemplateRef<any>;
54 @ViewChild('orchTmpl', { static: true })
55 orchTmpl: TemplateRef<any>;
56 @ViewChild('flashTmpl', { static: true })
57 flashTmpl: TemplateRef<any>;
58
59 @Input()
60 hiddenColumns: string[] = [];
61
62 @Input()
63 hideTitle = false;
64
65 @Input()
66 hideSubmitBtn = false;
67
68 @Input()
69 hasTableDetails = true;
70
71 @Input()
72 hideToolHeader = false;
73
74 @Input()
75 showGeneralActionsOnly = false;
76
77 permissions: Permissions;
78 columns: Array<CdTableColumn> = [];
79 hosts: Array<object> = [];
80 isLoadingHosts = false;
81 cdParams = { fromLink: '/hosts' };
82 tableActions: CdTableAction[];
83 selection = new CdTableSelection();
84 modalRef: NgbModalRef;
85 isExecuting = false;
86 errorMessage: string;
87 enableButton: boolean;
88 bsModalRef: NgbModalRef;
89
90 icons = Icons;
91
92 messages = {
93 nonOrchHost: $localize`The feature is disabled because the selected host is not managed by Orchestrator.`
94 };
95
96 orchStatus: OrchestratorStatus;
97 actionOrchFeatures = {
98 add: [OrchestratorFeature.HOST_ADD],
99 edit: [OrchestratorFeature.HOST_LABEL_ADD, OrchestratorFeature.HOST_LABEL_REMOVE],
100 remove: [OrchestratorFeature.HOST_REMOVE],
101 maintenance: [
102 OrchestratorFeature.HOST_MAINTENANCE_ENTER,
103 OrchestratorFeature.HOST_MAINTENANCE_EXIT
104 ]
105 };
106
107 constructor(
108 private authStorageService: AuthStorageService,
109 private dimlessBinary: DimlessBinaryPipe,
110 private hostService: HostService,
111 private actionLabels: ActionLabelsI18n,
112 private modalService: ModalService,
113 private taskWrapper: TaskWrapperService,
114 private router: Router,
115 private notificationService: NotificationService,
116 private orchService: OrchestratorService
117 ) {
118 super();
119 this.permissions = this.authStorageService.getPermissions();
120 this.tableActions = [
121 {
122 name: this.actionLabels.ADD,
123 permission: 'create',
124 icon: Icons.add,
125 click: () =>
126 this.router.url.includes('/hosts')
127 ? this.router.navigate([BASE_URL, { outlets: { modal: [URLVerbs.ADD] } }])
128 : (this.bsModalRef = this.modalService.show(HostFormComponent)),
129 disable: (selection: CdTableSelection) => this.getDisable('add', selection)
130 },
131 {
132 name: this.actionLabels.EDIT,
133 permission: 'update',
134 icon: Icons.edit,
135 click: () => this.editAction(),
136 disable: (selection: CdTableSelection) => this.getDisable('edit', selection)
137 },
138 {
139 name: this.actionLabels.REMOVE,
140 permission: 'delete',
141 icon: Icons.destroy,
142 click: () => this.deleteAction(),
143 disable: (selection: CdTableSelection) => this.getDisable('remove', selection)
144 },
145 {
146 name: this.actionLabels.ENTER_MAINTENANCE,
147 permission: 'update',
148 icon: Icons.enter,
149 click: () => this.hostMaintenance(),
150 disable: (selection: CdTableSelection) =>
151 this.getDisable('maintenance', selection) || this.isExecuting || this.enableButton,
152 visible: () => !this.showGeneralActionsOnly
153 },
154 {
155 name: this.actionLabels.EXIT_MAINTENANCE,
156 permission: 'update',
157 icon: Icons.exit,
158 click: () => this.hostMaintenance(),
159 disable: (selection: CdTableSelection) =>
160 this.getDisable('maintenance', selection) || this.isExecuting || !this.enableButton,
161 visible: () => !this.showGeneralActionsOnly
162 }
163 ];
164 }
165
166 ngOnInit() {
167 this.columns = [
168 {
169 name: $localize`Hostname`,
170 prop: 'hostname',
171 flexGrow: 1
172 },
173 {
174 name: $localize`Services`,
175 prop: 'services',
176 flexGrow: 2,
177 cellTemplate: this.servicesTpl
178 },
179 {
180 name: $localize`Labels`,
181 prop: 'labels',
182 flexGrow: 1,
183 cellTransformation: CellTemplate.badge,
184 customTemplateConfig: {
185 class: 'badge-dark'
186 }
187 },
188 {
189 name: $localize`Status`,
190 prop: 'status',
191 flexGrow: 1,
192 cellTransformation: CellTemplate.badge,
193 customTemplateConfig: {
194 map: {
195 maintenance: { class: 'badge-warning' }
196 }
197 }
198 },
199 {
200 name: $localize`Model`,
201 prop: 'model',
202 flexGrow: 1
203 },
204 {
205 name: $localize`CPUs`,
206 prop: 'cpu_count',
207 flexGrow: 0.3
208 },
209 {
210 name: $localize`Cores`,
211 prop: 'cpu_cores',
212 flexGrow: 0.3
213 },
214 {
215 name: $localize`Total Memory`,
216 prop: 'memory_total_bytes',
217 pipe: this.dimlessBinary,
218 flexGrow: 0.4
219 },
220 {
221 name: $localize`Raw Capacity`,
222 prop: 'raw_capacity',
223 pipe: this.dimlessBinary,
224 flexGrow: 0.5
225 },
226 {
227 name: $localize`HDDs`,
228 prop: 'hdd_count',
229 flexGrow: 0.3
230 },
231 {
232 name: $localize`Flash`,
233 prop: 'flash_count',
234 headerTemplate: this.flashTmpl,
235 flexGrow: 0.3
236 },
237 {
238 name: $localize`NICs`,
239 prop: 'nic_count',
240 flexGrow: 0.3
241 }
242 ];
243
244 this.columns = this.columns.filter((col: any) => {
245 return !this.hiddenColumns.includes(col.prop);
246 });
247 }
248
249 ngOnDestroy() {
250 this.sub.unsubscribe();
251 }
252
253 updateSelection(selection: CdTableSelection) {
254 this.selection = selection;
255 this.enableButton = false;
256 if (this.selection.hasSelection) {
257 if (this.selection.first().status === 'maintenance') {
258 this.enableButton = true;
259 }
260 }
261 }
262
263 editAction() {
264 this.hostService.getLabels().subscribe((resp: string[]) => {
265 const host = this.selection.first();
266 const labels = new Set(resp.concat(this.hostService.predefinedLabels));
267 const allLabels = Array.from(labels).map((label) => {
268 return { enabled: true, name: label };
269 });
270 this.modalService.show(FormModalComponent, {
271 titleText: $localize`Edit Host: ${host.hostname}`,
272 fields: [
273 {
274 type: 'select-badges',
275 name: 'labels',
276 value: host['labels'],
277 label: $localize`Labels`,
278 typeConfig: {
279 customBadges: true,
280 options: allLabels,
281 messages: new SelectMessages({
282 empty: $localize`There are no labels.`,
283 filter: $localize`Filter or add labels`,
284 add: $localize`Add label`
285 })
286 }
287 }
288 ],
289 submitButtonText: $localize`Edit Host`,
290 onSubmit: (values: any) => {
291 this.hostService.update(host['hostname'], true, values.labels).subscribe(() => {
292 this.notificationService.show(
293 NotificationType.success,
294 $localize`Updated Host "${host.hostname}"`
295 );
296 // Reload the data table content.
297 this.table.refreshBtn();
298 });
299 }
300 });
301 });
302 }
303
304 hostMaintenance() {
305 this.isExecuting = true;
306 const host = this.selection.first();
307 if (host['status'] !== 'maintenance') {
308 this.hostService.update(host['hostname'], false, [], true).subscribe(
309 () => {
310 this.isExecuting = false;
311 this.notificationService.show(
312 NotificationType.success,
313 $localize`"${host.hostname}" moved to maintenance`
314 );
315 this.table.refreshBtn();
316 },
317 (error) => {
318 this.isExecuting = false;
319 this.errorMessage = error.error['detail'].split(/\n/);
320 error.preventDefault();
321 if (
322 error.error['detail'].includes('WARNING') &&
323 !error.error['detail'].includes('It is NOT safe to stop') &&
324 !error.error['detail'].includes('ALERT') &&
325 !error.error['detail'].includes('unsafe to stop')
326 ) {
327 const modalVariables = {
328 titleText: $localize`Warning`,
329 buttonText: $localize`Continue`,
330 warning: true,
331 bodyTpl: this.maintenanceConfirmTpl,
332 showSubmit: true,
333 onSubmit: () => {
334 this.hostService.update(host['hostname'], false, [], true, true).subscribe(
335 () => {
336 this.modalRef.close();
337 },
338 () => this.modalRef.close()
339 );
340 }
341 };
342 this.modalRef = this.modalService.show(ConfirmationModalComponent, modalVariables);
343 } else {
344 this.notificationService.show(
345 NotificationType.error,
346 $localize`"${host.hostname}" cannot be put into maintenance`,
347 $localize`${error.error['detail']}`
348 );
349 }
350 }
351 );
352 } else {
353 this.hostService.update(host['hostname'], false, [], true).subscribe(() => {
354 this.isExecuting = false;
355 this.notificationService.show(
356 NotificationType.success,
357 $localize`"${host.hostname}" has exited maintenance`
358 );
359 this.table.refreshBtn();
360 });
361 }
362 }
363
364 getDisable(
365 action: 'add' | 'edit' | 'remove' | 'maintenance',
366 selection: CdTableSelection
367 ): boolean | string {
368 if (action === 'remove' || action === 'edit' || action === 'maintenance') {
369 if (!selection?.hasSingleSelection) {
370 return true;
371 }
372 if (!_.every(selection.selected, 'sources.orchestrator')) {
373 return this.messages.nonOrchHost;
374 }
375 }
376 return this.orchService.getTableActionDisableDesc(
377 this.orchStatus,
378 this.actionOrchFeatures[action]
379 );
380 }
381
382 deleteAction() {
383 const hostname = this.selection.first().hostname;
384 this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
385 itemDescription: 'Host',
386 itemNames: [hostname],
387 actionDescription: 'remove',
388 submitActionObservable: () =>
389 this.taskWrapper.wrapTaskAroundCall({
390 task: new FinishedTask('host/remove', { hostname: hostname }),
391 call: this.hostService.delete(hostname)
392 })
393 });
394 }
395
396 checkHostsFactsAvailable() {
397 const orchFeatures = this.orchStatus.features;
398 if (!_.isEmpty(orchFeatures)) {
399 if (orchFeatures.get_facts.available) {
400 return true;
401 }
402 return false;
403 }
404 return false;
405 }
406
407 transformHostsData() {
408 if (this.checkHostsFactsAvailable()) {
409 _.forEach(this.hosts, (hostKey) => {
410 hostKey['memory_total_bytes'] = hostKey['memory_total_kb'] * 1024;
411 hostKey['raw_capacity'] = hostKey['hdd_capacity_bytes'] + hostKey['flash_capacity_bytes'];
412 });
413 } else {
414 // mark host facts columns unavailable
415 for (let column = 4; column < this.columns.length; column++) {
416 this.columns[column]['prop'] = '';
417 this.columns[column]['cellTemplate'] = this.orchTmpl;
418 }
419 }
420 }
421
422 getHosts(context: CdTableFetchDataContext) {
423 if (this.isLoadingHosts) {
424 return;
425 }
426 const typeToPermissionKey = {
427 mds: 'cephfs',
428 mon: 'monitor',
429 osd: 'osd',
430 rgw: 'rgw',
431 'rbd-mirror': 'rbdMirroring',
432 mgr: 'manager',
433 'tcmu-runner': 'iscsi'
434 };
435 this.isLoadingHosts = true;
436 this.sub = this.orchService
437 .status()
438 .pipe(
439 mergeMap((orchStatus) => {
440 this.orchStatus = orchStatus;
441 const factsAvailable = this.checkHostsFactsAvailable();
442 return this.hostService.list(`${factsAvailable}`);
443 }),
444 map((hostList: object[]) =>
445 hostList.map((host) => {
446 host['services'].map((service: any) => {
447 service.cdLink = `/perf_counters/${service.type}/${encodeURIComponent(service.id)}`;
448 const permission = this.permissions[typeToPermissionKey[service.type]];
449 service.canRead = permission ? permission.read : false;
450 return service;
451 });
452 return host;
453 })
454 )
455 )
456 .subscribe(
457 (hostList) => {
458 this.hosts = hostList;
459 this.transformHostsData();
460 this.isLoadingHosts = false;
461 },
462 () => {
463 this.isLoadingHosts = false;
464 context.error();
465 }
466 );
467 }
468 }