]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts
import ceph pacific 16.2.5
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / cluster / hosts / hosts.component.ts
1 import { Component, 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
7 import { HostService } from '~/app/shared/api/host.service';
8 import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
9 import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
10 import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
11 import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
12 import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
13 import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
14 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
15 import { TableComponent } from '~/app/shared/datatable/table/table.component';
16 import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
17 import { Icons } from '~/app/shared/enum/icons.enum';
18 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
19 import { CdTableAction } from '~/app/shared/models/cd-table-action';
20 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
21 import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
22 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
23 import { FinishedTask } from '~/app/shared/models/finished-task';
24 import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum';
25 import { OrchestratorStatus } from '~/app/shared/models/orchestrator.interface';
26 import { Permissions } from '~/app/shared/models/permissions';
27 import { CephShortVersionPipe } from '~/app/shared/pipes/ceph-short-version.pipe';
28 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
29 import { ModalService } from '~/app/shared/services/modal.service';
30 import { NotificationService } from '~/app/shared/services/notification.service';
31 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
32 import { URLBuilderService } from '~/app/shared/services/url-builder.service';
33
34 const BASE_URL = 'hosts';
35
36 @Component({
37 selector: 'cd-hosts',
38 templateUrl: './hosts.component.html',
39 styleUrls: ['./hosts.component.scss'],
40 providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
41 })
42 export class HostsComponent extends ListWithDetails implements OnInit {
43 @ViewChild(TableComponent)
44 table: TableComponent;
45 @ViewChild('servicesTpl', { static: true })
46 public servicesTpl: TemplateRef<any>;
47 @ViewChild('maintenanceConfirmTpl', { static: true })
48 maintenanceConfirmTpl: TemplateRef<any>;
49
50 permissions: Permissions;
51 columns: Array<CdTableColumn> = [];
52 hosts: Array<object> = [];
53 isLoadingHosts = false;
54 cdParams = { fromLink: '/hosts' };
55 tableActions: CdTableAction[];
56 selection = new CdTableSelection();
57 modalRef: NgbModalRef;
58 isExecuting = false;
59 errorMessage: string;
60 enableButton: boolean;
61
62 icons = Icons;
63
64 messages = {
65 nonOrchHost: $localize`The feature is disabled because the selected host is not managed by Orchestrator.`
66 };
67
68 orchStatus: OrchestratorStatus;
69 actionOrchFeatures = {
70 create: [OrchestratorFeature.HOST_CREATE],
71 edit: [OrchestratorFeature.HOST_LABEL_ADD, OrchestratorFeature.HOST_LABEL_REMOVE],
72 delete: [OrchestratorFeature.HOST_DELETE],
73 maintenance: [
74 OrchestratorFeature.HOST_MAINTENANCE_ENTER,
75 OrchestratorFeature.HOST_MAINTENANCE_EXIT
76 ]
77 };
78
79 constructor(
80 private authStorageService: AuthStorageService,
81 private hostService: HostService,
82 private cephShortVersionPipe: CephShortVersionPipe,
83 private urlBuilder: URLBuilderService,
84 private actionLabels: ActionLabelsI18n,
85 private modalService: ModalService,
86 private taskWrapper: TaskWrapperService,
87 private router: Router,
88 private notificationService: NotificationService,
89 private orchService: OrchestratorService
90 ) {
91 super();
92 this.permissions = this.authStorageService.getPermissions();
93 this.tableActions = [
94 {
95 name: this.actionLabels.CREATE,
96 permission: 'create',
97 icon: Icons.add,
98 click: () => this.router.navigate([this.urlBuilder.getCreate()]),
99 disable: (selection: CdTableSelection) => this.getDisable('create', selection)
100 },
101 {
102 name: this.actionLabels.EDIT,
103 permission: 'update',
104 icon: Icons.edit,
105 click: () => this.editAction(),
106 disable: (selection: CdTableSelection) => this.getDisable('edit', selection)
107 },
108 {
109 name: this.actionLabels.DELETE,
110 permission: 'delete',
111 icon: Icons.destroy,
112 click: () => this.deleteAction(),
113 disable: (selection: CdTableSelection) => this.getDisable('delete', selection)
114 },
115 {
116 name: this.actionLabels.ENTER_MAINTENANCE,
117 permission: 'update',
118 icon: Icons.enter,
119 click: () => this.hostMaintenance(),
120 disable: (selection: CdTableSelection) =>
121 this.getDisable('maintenance', selection) || this.isExecuting || this.enableButton
122 },
123 {
124 name: this.actionLabels.EXIT_MAINTENANCE,
125 permission: 'update',
126 icon: Icons.exit,
127 click: () => this.hostMaintenance(),
128 disable: (selection: CdTableSelection) =>
129 this.getDisable('maintenance', selection) || this.isExecuting || !this.enableButton
130 }
131 ];
132 }
133
134 ngOnInit() {
135 this.columns = [
136 {
137 name: $localize`Hostname`,
138 prop: 'hostname',
139 flexGrow: 1
140 },
141 {
142 name: $localize`Services`,
143 prop: 'services',
144 flexGrow: 3,
145 cellTemplate: this.servicesTpl
146 },
147 {
148 name: $localize`Labels`,
149 prop: 'labels',
150 flexGrow: 1,
151 cellTransformation: CellTemplate.badge,
152 customTemplateConfig: {
153 class: 'badge-dark'
154 }
155 },
156 {
157 name: $localize`Status`,
158 prop: 'status',
159 flexGrow: 1,
160 cellTransformation: CellTemplate.badge,
161 customTemplateConfig: {
162 map: {
163 maintenance: { class: 'badge-warning' }
164 }
165 }
166 },
167 {
168 name: $localize`Version`,
169 prop: 'ceph_version',
170 flexGrow: 1,
171 pipe: this.cephShortVersionPipe
172 }
173 ];
174 this.orchService.status().subscribe((status: OrchestratorStatus) => {
175 this.orchStatus = status;
176 });
177 }
178
179 updateSelection(selection: CdTableSelection) {
180 this.selection = selection;
181 this.enableButton = false;
182 if (this.selection.hasSelection) {
183 if (this.selection.first().status === 'maintenance') {
184 this.enableButton = true;
185 }
186 }
187 }
188
189 editAction() {
190 this.hostService.getLabels().subscribe((resp: string[]) => {
191 const host = this.selection.first();
192 const allLabels = resp.map((label) => {
193 return { enabled: true, name: label };
194 });
195 this.modalService.show(FormModalComponent, {
196 titleText: $localize`Edit Host: ${host.hostname}`,
197 fields: [
198 {
199 type: 'select-badges',
200 name: 'labels',
201 value: host['labels'],
202 label: $localize`Labels`,
203 typeConfig: {
204 customBadges: true,
205 options: allLabels,
206 messages: new SelectMessages({
207 empty: $localize`There are no labels.`,
208 filter: $localize`Filter or add labels`,
209 add: $localize`Add label`
210 })
211 }
212 }
213 ],
214 submitButtonText: $localize`Edit Host`,
215 onSubmit: (values: any) => {
216 this.hostService.update(host['hostname'], true, values.labels).subscribe(() => {
217 this.notificationService.show(
218 NotificationType.success,
219 $localize`Updated Host "${host.hostname}"`
220 );
221 // Reload the data table content.
222 this.table.refreshBtn();
223 });
224 }
225 });
226 });
227 }
228
229 hostMaintenance() {
230 this.isExecuting = true;
231 const host = this.selection.first();
232 if (host['status'] !== 'maintenance') {
233 this.hostService.update(host['hostname'], false, [], true).subscribe(
234 () => {
235 this.isExecuting = false;
236 this.notificationService.show(
237 NotificationType.success,
238 $localize`"${host.hostname}" moved to maintenance`
239 );
240 this.table.refreshBtn();
241 },
242 (error) => {
243 this.isExecuting = false;
244 this.errorMessage = error.error['detail'].split(/\n/);
245 error.preventDefault();
246 if (
247 error.error['detail'].includes('WARNING') &&
248 !error.error['detail'].includes('It is NOT safe to stop') &&
249 !error.error['detail'].includes('ALERT') &&
250 !error.error['detail'].includes('unable to stop')
251 ) {
252 const modalVarialbes = {
253 titleText: $localize`Warning`,
254 buttonText: $localize`Continue`,
255 warning: true,
256 bodyTpl: this.maintenanceConfirmTpl,
257 showSubmit: true,
258 onSubmit: () => {
259 this.hostService.update(host['hostname'], false, [], true, true).subscribe(
260 () => {
261 this.modalRef.close();
262 },
263 () => this.modalRef.close()
264 );
265 }
266 };
267 this.modalRef = this.modalService.show(ConfirmationModalComponent, modalVarialbes);
268 } else {
269 this.notificationService.show(
270 NotificationType.error,
271 $localize`"${host.hostname}" cannot be put into maintenance`,
272 $localize`${error.error['detail']}`
273 );
274 }
275 }
276 );
277 } else {
278 this.hostService.update(host['hostname'], false, [], true).subscribe(() => {
279 this.isExecuting = false;
280 this.notificationService.show(
281 NotificationType.success,
282 $localize`"${host.hostname}" has exited maintenance`
283 );
284 this.table.refreshBtn();
285 });
286 }
287 }
288
289 getDisable(
290 action: 'create' | 'edit' | 'delete' | 'maintenance',
291 selection: CdTableSelection
292 ): boolean | string {
293 if (action === 'delete' || action === 'edit' || action === 'maintenance') {
294 if (!selection?.hasSingleSelection) {
295 return true;
296 }
297 if (!_.every(selection.selected, 'sources.orchestrator')) {
298 return this.messages.nonOrchHost;
299 }
300 }
301 return this.orchService.getTableActionDisableDesc(
302 this.orchStatus,
303 this.actionOrchFeatures[action]
304 );
305 }
306
307 deleteAction() {
308 const hostname = this.selection.first().hostname;
309 this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
310 itemDescription: 'Host',
311 itemNames: [hostname],
312 actionDescription: 'delete',
313 submitActionObservable: () =>
314 this.taskWrapper.wrapTaskAroundCall({
315 task: new FinishedTask('host/delete', { hostname: hostname }),
316 call: this.hostService.delete(hostname)
317 })
318 });
319 }
320
321 getHosts(context: CdTableFetchDataContext) {
322 if (this.isLoadingHosts) {
323 return;
324 }
325 const typeToPermissionKey = {
326 mds: 'cephfs',
327 mon: 'monitor',
328 osd: 'osd',
329 rgw: 'rgw',
330 'rbd-mirror': 'rbdMirroring',
331 mgr: 'manager',
332 'tcmu-runner': 'iscsi'
333 };
334 this.isLoadingHosts = true;
335 this.hostService.list().subscribe(
336 (resp: any[]) => {
337 resp.map((host) => {
338 host.services.map((service: any) => {
339 service.cdLink = `/perf_counters/${service.type}/${encodeURIComponent(service.id)}`;
340 const permission = this.permissions[typeToPermissionKey[service.type]];
341 service.canRead = permission ? permission.read : false;
342 return service;
343 });
344 return host;
345 });
346 this.hosts = resp;
347 this.isLoadingHosts = false;
348 },
349 () => {
350 this.isLoadingHosts = false;
351 context.error();
352 }
353 );
354 }
355 }