]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts
0d691f62acfd826d19541182b228709e3ee4aaa3
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / cluster / hosts / hosts.component.spec.ts
1 import { HttpClientTestingModule } from '@angular/common/http/testing';
2 import { ComponentFixture, TestBed } from '@angular/core/testing';
3 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
4 import { RouterTestingModule } from '@angular/router/testing';
5
6 import { ToastrModule } from 'ngx-toastr';
7 import { of } from 'rxjs';
8
9 import { CephModule } from '~/app/ceph/ceph.module';
10 import { CephSharedModule } from '~/app/ceph/shared/ceph-shared.module';
11 import { CoreModule } from '~/app/core/core.module';
12 import { HostService } from '~/app/shared/api/host.service';
13 import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
14 import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
15 import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
16 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
17 import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum';
18 import { Permissions } from '~/app/shared/models/permissions';
19 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
20 import { SharedModule } from '~/app/shared/shared.module';
21 import {
22 configureTestBed,
23 OrchestratorHelper,
24 TableActionHelper
25 } from '~/testing/unit-test-helper';
26 import { HostsComponent } from './hosts.component';
27
28 class MockShowForceMaintenanceModal {
29 showModal = false;
30 showModalDialog(msg: string) {
31 if (
32 msg.includes('WARNING') &&
33 !msg.includes('It is NOT safe to stop') &&
34 !msg.includes('ALERT') &&
35 !msg.includes('unsafe to stop')
36 ) {
37 this.showModal = true;
38 }
39 }
40 }
41
42 describe('HostsComponent', () => {
43 let component: HostsComponent;
44 let fixture: ComponentFixture<HostsComponent>;
45 let hostListSpy: jasmine.Spy;
46 let orchService: OrchestratorService;
47 let showForceMaintenanceModal: MockShowForceMaintenanceModal;
48
49 const fakeAuthStorageService = {
50 getPermissions: () => {
51 return new Permissions({ hosts: ['read', 'update', 'create', 'delete'] });
52 }
53 };
54
55 configureTestBed({
56 imports: [
57 BrowserAnimationsModule,
58 CephSharedModule,
59 SharedModule,
60 HttpClientTestingModule,
61 RouterTestingModule,
62 ToastrModule.forRoot(),
63 CephModule,
64 CoreModule
65 ],
66 providers: [
67 { provide: AuthStorageService, useValue: fakeAuthStorageService },
68 TableActionsComponent
69 ]
70 });
71
72 beforeEach(() => {
73 showForceMaintenanceModal = new MockShowForceMaintenanceModal();
74 fixture = TestBed.createComponent(HostsComponent);
75 component = fixture.componentInstance;
76 hostListSpy = spyOn(TestBed.inject(HostService), 'list');
77 orchService = TestBed.inject(OrchestratorService);
78 });
79
80 it('should create', () => {
81 expect(component).toBeTruthy();
82 });
83
84 it('should render hosts list even with not permission mapped services', () => {
85 const hostname = 'ceph.dev';
86 const payload = [
87 {
88 services: [
89 {
90 type: 'osd',
91 id: '0'
92 },
93 {
94 type: 'rgw',
95 id: 'rgw'
96 },
97 {
98 type: 'notPermissionMappedService',
99 id: '1'
100 }
101 ],
102 hostname: hostname,
103 labels: ['foo', 'bar']
104 }
105 ];
106
107 OrchestratorHelper.mockStatus(false);
108 hostListSpy.and.callFake(() => of(payload));
109 fixture.detectChanges();
110
111 component.getHosts(new CdTableFetchDataContext(() => undefined));
112 fixture.detectChanges();
113
114 const spans = fixture.debugElement.nativeElement.querySelectorAll(
115 '.datatable-body-cell-label span'
116 );
117 expect(spans[0].textContent).toBe(hostname);
118 });
119
120 it('should show the exact count of the repeating daemons', () => {
121 const hostname = 'ceph.dev';
122 const payload = [
123 {
124 services: [
125 {
126 type: 'mgr',
127 id: 'x'
128 },
129 {
130 type: 'mgr',
131 id: 'y'
132 },
133 {
134 type: 'osd',
135 id: '0'
136 },
137 {
138 type: 'osd',
139 id: '1'
140 },
141 {
142 type: 'osd',
143 id: '2'
144 },
145 {
146 type: 'rgw',
147 id: 'rgw'
148 }
149 ],
150 hostname: hostname,
151 labels: ['foo', 'bar']
152 }
153 ];
154
155 OrchestratorHelper.mockStatus(false);
156 hostListSpy.and.callFake(() => of(payload));
157 fixture.detectChanges();
158
159 component.getHosts(new CdTableFetchDataContext(() => undefined));
160 fixture.detectChanges();
161
162 const spans = fixture.debugElement.nativeElement.querySelectorAll(
163 '.datatable-body-cell-label span span.badge.badge-background-primary'
164 );
165 expect(spans[0].textContent).toContain('mgr: 2');
166 expect(spans[1].textContent).toContain('osd: 3');
167 expect(spans[2].textContent).toContain('rgw: 1');
168 });
169
170 it('should test if host facts are tranformed correctly if orch available', () => {
171 const features = [OrchestratorFeature.HOST_FACTS];
172 const payload = [
173 {
174 hostname: 'host_test',
175 services: [
176 {
177 type: 'osd',
178 id: '0'
179 }
180 ],
181 cpu_count: 2,
182 cpu_cores: 1,
183 memory_total_kb: 1024,
184 hdd_count: 4,
185 hdd_capacity_bytes: 1024,
186 flash_count: 4,
187 flash_capacity_bytes: 1024,
188 nic_count: 1
189 }
190 ];
191 OrchestratorHelper.mockStatus(true, features);
192 hostListSpy.and.callFake(() => of(payload));
193 fixture.detectChanges();
194
195 component.getHosts(new CdTableFetchDataContext(() => undefined));
196 expect(hostListSpy).toHaveBeenCalled();
197 expect(component.hosts[0]['cpu_count']).toEqual(2);
198 expect(component.hosts[0]['memory_total_bytes']).toEqual(1048576);
199 expect(component.hosts[0]['raw_capacity']).toEqual(2048);
200 expect(component.hosts[0]['hdd_count']).toEqual(4);
201 expect(component.hosts[0]['flash_count']).toEqual(4);
202 expect(component.hosts[0]['cpu_cores']).toEqual(1);
203 expect(component.hosts[0]['nic_count']).toEqual(1);
204 });
205
206 it('should test if host facts are unavailable if no orch available', () => {
207 const payload = [
208 {
209 hostname: 'host_test',
210 services: [
211 {
212 type: 'osd',
213 id: '0'
214 }
215 ]
216 }
217 ];
218 OrchestratorHelper.mockStatus(false);
219 hostListSpy.and.callFake(() => of(payload));
220 fixture.detectChanges();
221
222 component.getHosts(new CdTableFetchDataContext(() => undefined));
223 fixture.detectChanges();
224
225 const spans = fixture.debugElement.nativeElement.querySelectorAll(
226 '.datatable-body-cell-label span'
227 );
228 expect(spans[7].textContent).toBe('N/A');
229 });
230
231 it('should test if host facts are unavailable if get_fatcs orch feature is not available', () => {
232 const payload = [
233 {
234 hostname: 'host_test',
235 services: [
236 {
237 type: 'osd',
238 id: '0'
239 }
240 ]
241 }
242 ];
243 OrchestratorHelper.mockStatus(true);
244 hostListSpy.and.callFake(() => of(payload));
245 fixture.detectChanges();
246
247 component.getHosts(new CdTableFetchDataContext(() => undefined));
248 fixture.detectChanges();
249
250 const spans = fixture.debugElement.nativeElement.querySelectorAll(
251 '.datatable-body-cell-label span'
252 );
253 expect(spans[7].textContent).toBe('N/A');
254 });
255
256 it('should test if memory/raw capacity columns shows N/A if facts are available but in fetching state', () => {
257 const features = [OrchestratorFeature.HOST_FACTS];
258 let hostPayload: any[];
259 hostPayload = [
260 {
261 hostname: 'host_test',
262 services: [
263 {
264 type: 'osd',
265 id: '0'
266 }
267 ],
268 cpu_count: 2,
269 cpu_cores: 1,
270 memory_total_kb: undefined,
271 hdd_count: 4,
272 hdd_capacity_bytes: undefined,
273 flash_count: 4,
274 flash_capacity_bytes: undefined,
275 nic_count: 1
276 }
277 ];
278 OrchestratorHelper.mockStatus(true, features);
279 hostListSpy.and.callFake(() => of(hostPayload));
280 fixture.detectChanges();
281
282 component.getHosts(new CdTableFetchDataContext(() => undefined));
283 expect(component.hosts[0]['memory_total_bytes']).toEqual('N/A');
284 expect(component.hosts[0]['raw_capacity']).toEqual('N/A');
285 });
286
287 it('should show force maintenance modal when it is safe to stop host', () => {
288 const errorMsg = `WARNING: Stopping 1 out of 1 daemons in Grafana service.
289 Service will not be operational with no daemons left. At
290 least 1 daemon must be running to guarantee service.`;
291 showForceMaintenanceModal.showModalDialog(errorMsg);
292 expect(showForceMaintenanceModal.showModal).toBeTruthy();
293 });
294
295 it('should not show force maintenance modal when error is an ALERT', () => {
296 const errorMsg = `ALERT: Cannot stop active Mgr daemon, Please switch active Mgrs
297 with 'ceph mgr fail ceph-node-00'`;
298 showForceMaintenanceModal.showModalDialog(errorMsg);
299 expect(showForceMaintenanceModal.showModal).toBeFalsy();
300 });
301
302 it('should not show force maintenance modal when it is not safe to stop host', () => {
303 const errorMsg = `WARNING: Stopping 1 out of 1 daemons in Grafana service.
304 Service will not be operational with no daemons left. At
305 least 1 daemon must be running to guarantee service.
306 It is NOT safe to stop ['mon.ceph-node-00']: not enough
307 monitors would be available (ceph-node-02) after stopping mons`;
308 showForceMaintenanceModal.showModalDialog(errorMsg);
309 expect(showForceMaintenanceModal.showModal).toBeFalsy();
310 });
311
312 it('should not show force maintenance modal when it is unsafe to stop host', () => {
313 const errorMsg = 'unsafe to stop osd.0 because of some unknown reason';
314 showForceMaintenanceModal.showModalDialog(errorMsg);
315 expect(showForceMaintenanceModal.showModal).toBeFalsy();
316 });
317
318 describe('table actions', () => {
319 const fakeHosts = require('./fixtures/host_list_response.json');
320
321 beforeEach(() => {
322 hostListSpy.and.callFake(() => of(fakeHosts));
323 });
324
325 const testTableActions = async (
326 orch: boolean,
327 features: OrchestratorFeature[],
328 tests: { selectRow?: number; expectResults: any }[]
329 ) => {
330 OrchestratorHelper.mockStatus(orch, features);
331 fixture.detectChanges();
332 await fixture.whenStable();
333
334 for (const test of tests) {
335 if (test.selectRow) {
336 component.selection = new CdTableSelection();
337 component.selection.selected = [test.selectRow];
338 }
339 await TableActionHelper.verifyTableActions(
340 fixture,
341 component.tableActions,
342 test.expectResults
343 );
344 }
345 };
346
347 it('should have correct states when Orchestrator is enabled', async () => {
348 const tests = [
349 {
350 expectResults: {
351 Add: { disabled: false, disableDesc: '' },
352 Edit: { disabled: true, disableDesc: '' },
353 Remove: { disabled: true, disableDesc: '' }
354 }
355 },
356 {
357 selectRow: fakeHosts[0], // non-orchestrator host
358 expectResults: {
359 Add: { disabled: false, disableDesc: '' },
360 Edit: { disabled: true, disableDesc: component.messages.nonOrchHost },
361 Remove: { disabled: true, disableDesc: component.messages.nonOrchHost }
362 }
363 },
364 {
365 selectRow: fakeHosts[1], // orchestrator host
366 expectResults: {
367 Add: { disabled: false, disableDesc: '' },
368 Edit: { disabled: false, disableDesc: '' },
369 Remove: { disabled: false, disableDesc: '' }
370 }
371 }
372 ];
373
374 const features = [
375 OrchestratorFeature.HOST_ADD,
376 OrchestratorFeature.HOST_LABEL_ADD,
377 OrchestratorFeature.HOST_REMOVE,
378 OrchestratorFeature.HOST_LABEL_REMOVE,
379 OrchestratorFeature.HOST_DRAIN
380 ];
381 await testTableActions(true, features, tests);
382 });
383
384 it('should have correct states when Orchestrator is disabled', async () => {
385 const resultNoOrchestrator = {
386 disabled: true,
387 disableDesc: orchService.disableMessages.noOrchestrator
388 };
389 const tests = [
390 {
391 expectResults: {
392 Add: resultNoOrchestrator,
393 Edit: { disabled: true, disableDesc: '' },
394 Remove: { disabled: true, disableDesc: '' }
395 }
396 },
397 {
398 selectRow: fakeHosts[0], // non-orchestrator host
399 expectResults: {
400 Add: resultNoOrchestrator,
401 Edit: { disabled: true, disableDesc: component.messages.nonOrchHost },
402 Remove: { disabled: true, disableDesc: component.messages.nonOrchHost }
403 }
404 },
405 {
406 selectRow: fakeHosts[1], // orchestrator host
407 expectResults: {
408 Add: resultNoOrchestrator,
409 Edit: resultNoOrchestrator,
410 Remove: resultNoOrchestrator
411 }
412 }
413 ];
414 await testTableActions(false, [], tests);
415 });
416
417 it('should have correct states when Orchestrator features are missing', async () => {
418 const resultMissingFeatures = {
419 disabled: true,
420 disableDesc: orchService.disableMessages.missingFeature
421 };
422 const tests = [
423 {
424 expectResults: {
425 Add: resultMissingFeatures,
426 Edit: { disabled: true, disableDesc: '' },
427 Remove: { disabled: true, disableDesc: '' }
428 }
429 },
430 {
431 selectRow: fakeHosts[0], // non-orchestrator host
432 expectResults: {
433 Add: resultMissingFeatures,
434 Edit: { disabled: true, disableDesc: component.messages.nonOrchHost },
435 Remove: { disabled: true, disableDesc: component.messages.nonOrchHost }
436 }
437 },
438 {
439 selectRow: fakeHosts[1], // orchestrator host
440 expectResults: {
441 Add: resultMissingFeatures,
442 Edit: resultMissingFeatures,
443 Remove: resultMissingFeatures
444 }
445 }
446 ];
447 await testTableActions(true, [], tests);
448 });
449 });
450 });