]> git.proxmox.com Git - ceph.git/blame - 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
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;
87 enableButton: boolean;
a4b75251 88 bsModalRef: NgbModalRef;
f67539c2
TL
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 = {
a4b75251 98 add: [OrchestratorFeature.HOST_ADD],
f67539c2 99 edit: [OrchestratorFeature.HOST_LABEL_ADD, OrchestratorFeature.HOST_LABEL_REMOVE],
a4b75251 100 remove: [OrchestratorFeature.HOST_REMOVE],
f67539c2
TL
101 maintenance: [
102 OrchestratorFeature.HOST_MAINTENANCE_ENTER,
103 OrchestratorFeature.HOST_MAINTENANCE_EXIT
104 ]
105 };
11fdf7f2 106
11fdf7f2
TL
107 constructor(
108 private authStorageService: AuthStorageService,
a4b75251 109 private dimlessBinary: DimlessBinaryPipe,
11fdf7f2 110 private hostService: HostService,
9f95a23c 111 private actionLabels: ActionLabelsI18n,
f67539c2 112 private modalService: ModalService,
9f95a23c
TL
113 private taskWrapper: TaskWrapperService,
114 private router: Router,
f67539c2
TL
115 private notificationService: NotificationService,
116 private orchService: OrchestratorService
11fdf7f2 117 ) {
e306af50 118 super();
11fdf7f2 119 this.permissions = this.authStorageService.getPermissions();
9f95a23c
TL
120 this.tableActions = [
121 {
a4b75251 122 name: this.actionLabels.ADD,
9f95a23c
TL
123 permission: 'create',
124 icon: Icons.add,
a4b75251
TL
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)
9f95a23c 130 },
f6b5b4d7
TL
131 {
132 name: this.actionLabels.EDIT,
133 permission: 'update',
134 icon: Icons.edit,
f67539c2
TL
135 click: () => this.editAction(),
136 disable: (selection: CdTableSelection) => this.getDisable('edit', selection)
f6b5b4d7 137 },
9f95a23c 138 {
a4b75251 139 name: this.actionLabels.REMOVE,
9f95a23c
TL
140 permission: 'delete',
141 icon: Icons.destroy,
f67539c2 142 click: () => this.deleteAction(),
a4b75251 143 disable: (selection: CdTableSelection) => this.getDisable('remove', selection)
f67539c2
TL
144 },
145 {
146 name: this.actionLabels.ENTER_MAINTENANCE,
147 permission: 'update',
148 icon: Icons.enter,
149 click: () => this.hostMaintenance(),
150 disable: (selection: CdTableSelection) =>
a4b75251
TL
151 this.getDisable('maintenance', selection) || this.isExecuting || this.enableButton,
152 visible: () => !this.showGeneralActionsOnly
f67539c2
TL
153 },
154 {
155 name: this.actionLabels.EXIT_MAINTENANCE,
156 permission: 'update',
157 icon: Icons.exit,
158 click: () => this.hostMaintenance(),
159 disable: (selection: CdTableSelection) =>
a4b75251
TL
160 this.getDisable('maintenance', selection) || this.isExecuting || !this.enableButton,
161 visible: () => !this.showGeneralActionsOnly
9f95a23c
TL
162 }
163 ];
11fdf7f2
TL
164 }
165
166 ngOnInit() {
167 this.columns = [
168 {
f67539c2 169 name: $localize`Hostname`,
11fdf7f2
TL
170 prop: 'hostname',
171 flexGrow: 1
172 },
173 {
f67539c2 174 name: $localize`Services`,
11fdf7f2 175 prop: 'services',
a4b75251 176 flexGrow: 2,
11fdf7f2
TL
177 cellTemplate: this.servicesTpl
178 },
e306af50 179 {
f67539c2 180 name: $localize`Labels`,
e306af50
TL
181 prop: 'labels',
182 flexGrow: 1,
f67539c2
TL
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 }
e306af50 198 },
11fdf7f2 199 {
a4b75251
TL
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
11fdf7f2
TL
241 }
242 ];
a4b75251
TL
243
244 this.columns = this.columns.filter((col: any) => {
245 return !this.hiddenColumns.includes(col.prop);
f67539c2 246 });
11fdf7f2
TL
247 }
248
a4b75251
TL
249 ngOnDestroy() {
250 this.sub.unsubscribe();
251 }
252
11fdf7f2
TL
253 updateSelection(selection: CdTableSelection) {
254 this.selection = selection;
f67539c2
TL
255 this.enableButton = false;
256 if (this.selection.hasSelection) {
257 if (this.selection.first().status === 'maintenance') {
258 this.enableButton = true;
259 }
260 }
11fdf7f2
TL
261 }
262
f6b5b4d7
TL
263 editAction() {
264 this.hostService.getLabels().subscribe((resp: string[]) => {
265 const host = this.selection.first();
a4b75251
TL
266 const labels = new Set(resp.concat(this.hostService.predefinedLabels));
267 const allLabels = Array.from(labels).map((label) => {
f6b5b4d7
TL
268 return { enabled: true, name: label };
269 });
270 this.modalService.show(FormModalComponent, {
f67539c2
TL
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 })
f6b5b4d7 286 }
f6b5b4d7 287 }
f67539c2
TL
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 });
f6b5b4d7
TL
299 }
300 });
301 });
302 }
303
f67539c2
TL
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') &&
b3b6e05e 324 !error.error['detail'].includes('ALERT') &&
522d829b 325 !error.error['detail'].includes('unsafe to stop')
f67539c2 326 ) {
522d829b 327 const modalVariables = {
f67539c2
TL
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 };
522d829b 342 this.modalRef = this.modalService.show(ConfirmationModalComponent, modalVariables);
f67539c2
TL
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`
f91f0fd5 358 );
f67539c2
TL
359 this.table.refreshBtn();
360 });
f6b5b4d7 361 }
f67539c2 362 }
f91f0fd5 363
f67539c2 364 getDisable(
a4b75251 365 action: 'add' | 'edit' | 'remove' | 'maintenance',
f67539c2
TL
366 selection: CdTableSelection
367 ): boolean | string {
a4b75251 368 if (action === 'remove' || action === 'edit' || action === 'maintenance') {
f67539c2
TL
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 );
f6b5b4d7
TL
380 }
381
382 deleteAction() {
9f95a23c
TL
383 const hostname = this.selection.first().hostname;
384 this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
f67539c2
TL
385 itemDescription: 'Host',
386 itemNames: [hostname],
a4b75251 387 actionDescription: 'remove',
f67539c2
TL
388 submitActionObservable: () =>
389 this.taskWrapper.wrapTaskAroundCall({
a4b75251 390 task: new FinishedTask('host/remove', { hostname: hostname }),
f67539c2
TL
391 call: this.hostService.delete(hostname)
392 })
9f95a23c
TL
393 });
394 }
395
a4b75251
TL
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
11fdf7f2
TL
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;
a4b75251
TL
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 );
11fdf7f2
TL
467 }
468}