]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts
03a61e0834d034c2f2ca1d1edd3e90e935f307e8
[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 service_instances: [
125 {
126 type: 'mgr',
127 count: 2
128 },
129 {
130 type: 'osd',
131 count: 3
132 },
133 {
134 type: 'rgw',
135 count: 1
136 }
137 ],
138 hostname: hostname,
139 labels: ['foo', 'bar']
140 }
141 ];
142
143 OrchestratorHelper.mockStatus(false);
144 hostListSpy.and.callFake(() => of(payload));
145 fixture.detectChanges();
146
147 component.getHosts(new CdTableFetchDataContext(() => undefined));
148 fixture.detectChanges();
149
150 const spans = fixture.debugElement.nativeElement.querySelectorAll(
151 '.datatable-body-cell-label span span.badge.badge-background-primary'
152 );
153 expect(spans[0].textContent).toContain('mgr: 2');
154 expect(spans[1].textContent).toContain('osd: 3');
155 expect(spans[2].textContent).toContain('rgw: 1');
156 });
157
158 it('should test if host facts are tranformed correctly if orch available', () => {
159 const features = [OrchestratorFeature.HOST_FACTS];
160 const payload = [
161 {
162 hostname: 'host_test',
163 services: [
164 {
165 type: 'osd',
166 id: '0'
167 }
168 ],
169 cpu_count: 2,
170 cpu_cores: 1,
171 memory_total_kb: 1024,
172 hdd_count: 4,
173 hdd_capacity_bytes: 1024,
174 flash_count: 4,
175 flash_capacity_bytes: 1024,
176 nic_count: 1
177 }
178 ];
179 OrchestratorHelper.mockStatus(true, features);
180 hostListSpy.and.callFake(() => of(payload));
181 fixture.detectChanges();
182
183 component.getHosts(new CdTableFetchDataContext(() => undefined));
184 expect(hostListSpy).toHaveBeenCalled();
185 expect(component.hosts[0]['cpu_count']).toEqual(2);
186 expect(component.hosts[0]['memory_total_bytes']).toEqual(1048576);
187 expect(component.hosts[0]['raw_capacity']).toEqual(2048);
188 expect(component.hosts[0]['hdd_count']).toEqual(4);
189 expect(component.hosts[0]['flash_count']).toEqual(4);
190 expect(component.hosts[0]['cpu_cores']).toEqual(1);
191 expect(component.hosts[0]['nic_count']).toEqual(1);
192 });
193
194 it('should test if host facts are unavailable if no orch available', () => {
195 const payload = [
196 {
197 hostname: 'host_test',
198 services: [
199 {
200 type: 'osd',
201 id: '0'
202 }
203 ]
204 }
205 ];
206 OrchestratorHelper.mockStatus(false);
207 hostListSpy.and.callFake(() => of(payload));
208 fixture.detectChanges();
209
210 component.getHosts(new CdTableFetchDataContext(() => undefined));
211 fixture.detectChanges();
212
213 const spans = fixture.debugElement.nativeElement.querySelectorAll(
214 '.datatable-body-cell-label span'
215 );
216 expect(spans[7].textContent).toBe('N/A');
217 });
218
219 it('should test if host facts are unavailable if get_fatcs orch feature is not available', () => {
220 const payload = [
221 {
222 hostname: 'host_test',
223 services: [
224 {
225 type: 'osd',
226 id: '0'
227 }
228 ]
229 }
230 ];
231 OrchestratorHelper.mockStatus(true);
232 hostListSpy.and.callFake(() => of(payload));
233 fixture.detectChanges();
234
235 component.getHosts(new CdTableFetchDataContext(() => undefined));
236 fixture.detectChanges();
237
238 const spans = fixture.debugElement.nativeElement.querySelectorAll(
239 '.datatable-body-cell-label span'
240 );
241 expect(spans[7].textContent).toBe('N/A');
242 });
243
244 it('should test if memory/raw capacity columns shows N/A if facts are available but in fetching state', () => {
245 const features = [OrchestratorFeature.HOST_FACTS];
246 let hostPayload: any[];
247 hostPayload = [
248 {
249 hostname: 'host_test',
250 services: [
251 {
252 type: 'osd',
253 id: '0'
254 }
255 ],
256 cpu_count: 2,
257 cpu_cores: 1,
258 memory_total_kb: undefined,
259 hdd_count: 4,
260 hdd_capacity_bytes: undefined,
261 flash_count: 4,
262 flash_capacity_bytes: undefined,
263 nic_count: 1
264 }
265 ];
266 OrchestratorHelper.mockStatus(true, features);
267 hostListSpy.and.callFake(() => of(hostPayload));
268 fixture.detectChanges();
269
270 component.getHosts(new CdTableFetchDataContext(() => undefined));
271 expect(component.hosts[0]['memory_total_bytes']).toEqual('N/A');
272 expect(component.hosts[0]['raw_capacity']).toEqual('N/A');
273 });
274
275 it('should show force maintenance modal when it is safe to stop host', () => {
276 const errorMsg = `WARNING: Stopping 1 out of 1 daemons in Grafana service.
277 Service will not be operational with no daemons left. At
278 least 1 daemon must be running to guarantee service.`;
279 showForceMaintenanceModal.showModalDialog(errorMsg);
280 expect(showForceMaintenanceModal.showModal).toBeTruthy();
281 });
282
283 it('should not show force maintenance modal when error is an ALERT', () => {
284 const errorMsg = `ALERT: Cannot stop active Mgr daemon, Please switch active Mgrs
285 with 'ceph mgr fail ceph-node-00'`;
286 showForceMaintenanceModal.showModalDialog(errorMsg);
287 expect(showForceMaintenanceModal.showModal).toBeFalsy();
288 });
289
290 it('should not show force maintenance modal when it is not safe to stop host', () => {
291 const errorMsg = `WARNING: Stopping 1 out of 1 daemons in Grafana service.
292 Service will not be operational with no daemons left. At
293 least 1 daemon must be running to guarantee service.
294 It is NOT safe to stop ['mon.ceph-node-00']: not enough
295 monitors would be available (ceph-node-02) after stopping mons`;
296 showForceMaintenanceModal.showModalDialog(errorMsg);
297 expect(showForceMaintenanceModal.showModal).toBeFalsy();
298 });
299
300 it('should not show force maintenance modal when it is unsafe to stop host', () => {
301 const errorMsg = 'unsafe to stop osd.0 because of some unknown reason';
302 showForceMaintenanceModal.showModalDialog(errorMsg);
303 expect(showForceMaintenanceModal.showModal).toBeFalsy();
304 });
305
306 describe('table actions', () => {
307 const fakeHosts = require('./fixtures/host_list_response.json');
308
309 beforeEach(() => {
310 hostListSpy.and.callFake(() => of(fakeHosts));
311 });
312
313 const testTableActions = async (
314 orch: boolean,
315 features: OrchestratorFeature[],
316 tests: { selectRow?: number; expectResults: any }[]
317 ) => {
318 OrchestratorHelper.mockStatus(orch, features);
319 fixture.detectChanges();
320 await fixture.whenStable();
321
322 for (const test of tests) {
323 if (test.selectRow) {
324 component.selection = new CdTableSelection();
325 component.selection.selected = [test.selectRow];
326 }
327 await TableActionHelper.verifyTableActions(
328 fixture,
329 component.tableActions,
330 test.expectResults
331 );
332 }
333 };
334
335 it('should have correct states when Orchestrator is enabled', async () => {
336 const tests = [
337 {
338 expectResults: {
339 Add: { disabled: false, disableDesc: '' },
340 Edit: { disabled: true, disableDesc: '' },
341 Remove: { disabled: true, disableDesc: '' }
342 }
343 },
344 {
345 selectRow: fakeHosts[0], // non-orchestrator host
346 expectResults: {
347 Add: { disabled: false, disableDesc: '' },
348 Edit: { disabled: true, disableDesc: component.messages.nonOrchHost },
349 Remove: { disabled: true, disableDesc: component.messages.nonOrchHost }
350 }
351 },
352 {
353 selectRow: fakeHosts[1], // orchestrator host
354 expectResults: {
355 Add: { disabled: false, disableDesc: '' },
356 Edit: { disabled: false, disableDesc: '' },
357 Remove: { disabled: false, disableDesc: '' }
358 }
359 }
360 ];
361
362 const features = [
363 OrchestratorFeature.HOST_ADD,
364 OrchestratorFeature.HOST_LABEL_ADD,
365 OrchestratorFeature.HOST_REMOVE,
366 OrchestratorFeature.HOST_LABEL_REMOVE,
367 OrchestratorFeature.HOST_DRAIN
368 ];
369 await testTableActions(true, features, tests);
370 });
371
372 it('should have correct states when Orchestrator is disabled', async () => {
373 const resultNoOrchestrator = {
374 disabled: true,
375 disableDesc: orchService.disableMessages.noOrchestrator
376 };
377 const tests = [
378 {
379 expectResults: {
380 Add: resultNoOrchestrator,
381 Edit: { disabled: true, disableDesc: '' },
382 Remove: { disabled: true, disableDesc: '' }
383 }
384 },
385 {
386 selectRow: fakeHosts[0], // non-orchestrator host
387 expectResults: {
388 Add: resultNoOrchestrator,
389 Edit: { disabled: true, disableDesc: component.messages.nonOrchHost },
390 Remove: { disabled: true, disableDesc: component.messages.nonOrchHost }
391 }
392 },
393 {
394 selectRow: fakeHosts[1], // orchestrator host
395 expectResults: {
396 Add: resultNoOrchestrator,
397 Edit: resultNoOrchestrator,
398 Remove: resultNoOrchestrator
399 }
400 }
401 ];
402 await testTableActions(false, [], tests);
403 });
404
405 it('should have correct states when Orchestrator features are missing', async () => {
406 const resultMissingFeatures = {
407 disabled: true,
408 disableDesc: orchService.disableMessages.missingFeature
409 };
410 const tests = [
411 {
412 expectResults: {
413 Add: resultMissingFeatures,
414 Edit: { disabled: true, disableDesc: '' },
415 Remove: { disabled: true, disableDesc: '' }
416 }
417 },
418 {
419 selectRow: fakeHosts[0], // non-orchestrator host
420 expectResults: {
421 Add: resultMissingFeatures,
422 Edit: { disabled: true, disableDesc: component.messages.nonOrchHost },
423 Remove: { disabled: true, disableDesc: component.messages.nonOrchHost }
424 }
425 },
426 {
427 selectRow: fakeHosts[1], // orchestrator host
428 expectResults: {
429 Add: resultMissingFeatures,
430 Edit: resultMissingFeatures,
431 Remove: resultMissingFeatures
432 }
433 }
434 ];
435 await testTableActions(true, [], tests);
436 });
437 });
438 });