1 import { Component, Input, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
2 import { Router } from '@angular/router';
4 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
5 import _ from 'lodash';
6 import { Subscription } from 'rxjs';
7 import { map, mergeMap } from 'rxjs/operators';
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';
37 const BASE_URL = 'hosts';
41 templateUrl: './hosts.component.html',
42 styleUrls: ['./hosts.component.scss'],
43 providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
45 export class HostsComponent extends ListWithDetails implements OnDestroy, OnInit {
46 private sub = new Subscription();
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>;
60 hiddenColumns: string[] = [];
66 hideSubmitBtn = false;
69 hasTableDetails = true;
72 hideToolHeader = false;
75 showGeneralActionsOnly = false;
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;
87 enableButton: boolean;
88 bsModalRef: NgbModalRef;
93 nonOrchHost: $localize`The feature is disabled because the selected host is not managed by Orchestrator.`
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],
102 OrchestratorFeature.HOST_MAINTENANCE_ENTER,
103 OrchestratorFeature.HOST_MAINTENANCE_EXIT
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
119 this.permissions = this.authStorageService.getPermissions();
120 this.tableActions = [
122 name: this.actionLabels.ADD,
123 permission: 'create',
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)
132 name: this.actionLabels.EDIT,
133 permission: 'update',
135 click: () => this.editAction(),
136 disable: (selection: CdTableSelection) => this.getDisable('edit', selection)
139 name: this.actionLabels.REMOVE,
140 permission: 'delete',
142 click: () => this.deleteAction(),
143 disable: (selection: CdTableSelection) => this.getDisable('remove', selection)
146 name: this.actionLabels.ENTER_MAINTENANCE,
147 permission: 'update',
149 click: () => this.hostMaintenance(),
150 disable: (selection: CdTableSelection) =>
151 this.getDisable('maintenance', selection) || this.isExecuting || this.enableButton,
152 visible: () => !this.showGeneralActionsOnly
155 name: this.actionLabels.EXIT_MAINTENANCE,
156 permission: 'update',
158 click: () => this.hostMaintenance(),
159 disable: (selection: CdTableSelection) =>
160 this.getDisable('maintenance', selection) || this.isExecuting || !this.enableButton,
161 visible: () => !this.showGeneralActionsOnly
169 name: $localize`Hostname`,
174 name: $localize`Services`,
177 cellTemplate: this.servicesTpl
180 name: $localize`Labels`,
183 cellTransformation: CellTemplate.badge,
184 customTemplateConfig: {
189 name: $localize`Status`,
192 cellTransformation: CellTemplate.badge,
193 customTemplateConfig: {
195 maintenance: { class: 'badge-warning' }
200 name: $localize`Model`,
205 name: $localize`CPUs`,
210 name: $localize`Cores`,
215 name: $localize`Total Memory`,
216 prop: 'memory_total_bytes',
217 pipe: this.dimlessBinary,
221 name: $localize`Raw Capacity`,
222 prop: 'raw_capacity',
223 pipe: this.dimlessBinary,
227 name: $localize`HDDs`,
232 name: $localize`Flash`,
234 headerTemplate: this.flashTmpl,
238 name: $localize`NICs`,
244 this.columns = this.columns.filter((col: any) => {
245 return !this.hiddenColumns.includes(col.prop);
250 this.sub.unsubscribe();
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;
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 };
270 this.modalService.show(FormModalComponent, {
271 titleText: $localize`Edit Host: ${host.hostname}`,
274 type: 'select-badges',
276 value: host['labels'],
277 label: $localize`Labels`,
281 messages: new SelectMessages({
282 empty: $localize`There are no labels.`,
283 filter: $localize`Filter or add labels`,
284 add: $localize`Add label`
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}"`
296 // Reload the data table content.
297 this.table.refreshBtn();
305 this.isExecuting = true;
306 const host = this.selection.first();
307 if (host['status'] !== 'maintenance') {
308 this.hostService.update(host['hostname'], false, [], true).subscribe(
310 this.isExecuting = false;
311 this.notificationService.show(
312 NotificationType.success,
313 $localize`"${host.hostname}" moved to maintenance`
315 this.table.refreshBtn();
318 this.isExecuting = false;
319 this.errorMessage = error.error['detail'].split(/\n/);
320 error.preventDefault();
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')
327 const modalVariables = {
328 titleText: $localize`Warning`,
329 buttonText: $localize`Continue`,
331 bodyTpl: this.maintenanceConfirmTpl,
334 this.hostService.update(host['hostname'], false, [], true, true).subscribe(
336 this.modalRef.close();
338 () => this.modalRef.close()
342 this.modalRef = this.modalService.show(ConfirmationModalComponent, modalVariables);
344 this.notificationService.show(
345 NotificationType.error,
346 $localize`"${host.hostname}" cannot be put into maintenance`,
347 $localize`${error.error['detail']}`
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`
359 this.table.refreshBtn();
365 action: 'add' | 'edit' | 'remove' | 'maintenance',
366 selection: CdTableSelection
367 ): boolean | string {
368 if (action === 'remove' || action === 'edit' || action === 'maintenance') {
369 if (!selection?.hasSingleSelection) {
372 if (!_.every(selection.selected, 'sources.orchestrator')) {
373 return this.messages.nonOrchHost;
376 return this.orchService.getTableActionDisableDesc(
378 this.actionOrchFeatures[action]
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)
396 checkHostsFactsAvailable() {
397 const orchFeatures = this.orchStatus.features;
398 if (!_.isEmpty(orchFeatures)) {
399 if (orchFeatures.get_facts.available) {
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'];
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;
422 getHosts(context: CdTableFetchDataContext) {
423 if (this.isLoadingHosts) {
426 const typeToPermissionKey = {
431 'rbd-mirror': 'rbdMirroring',
433 'tcmu-runner': 'iscsi'
435 this.isLoadingHosts = true;
436 this.sub = this.orchService
439 mergeMap((orchStatus) => {
440 this.orchStatus = orchStatus;
441 const factsAvailable = this.checkHostsFactsAvailable();
442 return this.hostService.list(`${factsAvailable}`);
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;
458 this.hosts = hostList;
459 this.transformHostsData();
460 this.isLoadingHosts = false;
463 this.isLoadingHosts = false;