]>
Commit | Line | Data |
---|---|---|
a4b75251 | 1 | import { Component, Input, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; |
9f95a23c | 2 | import { Router } from '@angular/router'; |
11fdf7f2 | 3 | |
f67539c2 TL |
4 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; |
5 | import _ from 'lodash'; | |
a4b75251 TL |
6 | import { Subscription } from 'rxjs'; |
7 | import { map, mergeMap } from 'rxjs/operators'; | |
11fdf7f2 | 8 | |
f67539c2 TL |
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'; | |
a4b75251 | 16 | import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; |
f67539c2 TL |
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'; | |
a4b75251 | 29 | import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; |
f67539c2 TL |
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'; | |
a4b75251 | 35 | import { HostFormComponent } from './host-form/host-form.component'; |
9f95a23c TL |
36 | |
37 | const 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 |
45 | export 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 | } |