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