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