}
}
cy.get('cd-modal cd-submit-button').click();
+ this.checkLabelExists(hostname, labels, add);
+ }
+ checkLabelExists(hostname: string, labels: string[], add: boolean) {
// Verify labels are added or removed from Labels column
// First find row with hostname, then find labels in the row
this.getTableCell(this.columnIndex.hostname, hostname)
}
});
}
+
+ createOSD(deviceType: 'hdd' | 'ssd') {
+ // Click Primary devices Add button
+ cy.get('cd-osd-devices-selection-groups[name="Primary"]').as('primaryGroups');
+ cy.get('@primaryGroups').find('button').click();
+
+ // Select all devices with `deviceType`
+ cy.get('cd-osd-devices-selection-modal').within(() => {
+ cy.get('.modal-footer .tc_submitButton').as('addButton').should('be.disabled');
+ this.filterTable('Type', deviceType);
+ this.getTableCount('total').should('be.gte', 1);
+ cy.get('@addButton').click();
+ });
+ }
}
'ceph-node-02.cephlab.com'
];
const addHost = (hostname: string, exist?: boolean) => {
- createCluster.add(hostname, exist, true);
+ createCluster.add(hostname, exist, false);
createCluster.checkExist(hostname, true);
};
cy.get('.title').should('contain.text', 'Add Hosts');
});
- it('should check existing host and add new hosts into maintenance mode', () => {
+ it('should check existing host and add new hosts', () => {
createCluster.checkExist(hostnames[0], true);
addHost(hostnames[1], false);
addHost(hostnames[2], false);
});
+ it('should verify "_no_schedule" label is added', () => {
+ createCluster.checkLabelExists(hostnames[1], ['_no_schedule'], true);
+ createCluster.checkLabelExists(hostnames[2], ['_no_schedule'], true);
+ });
+
it('should not add an existing host', () => {
createCluster.add(hostnames[0], true);
});
--- /dev/null
+import { CreateClusterWizardHelper } from 'cypress/integration/cluster/create-cluster.po';
+import { OSDsPageHelper } from 'cypress/integration/cluster/osds.po';
+
+const osds = new OSDsPageHelper();
+
+describe('Create cluster create osds page', () => {
+ const createCluster = new CreateClusterWizardHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ createCluster.navigateTo();
+ createCluster.createCluster();
+ cy.get('button[aria-label="Next"]').click();
+ });
+
+ it('should check if nav-link and title contains Create OSDs', () => {
+ cy.get('.nav-link').should('contain.text', 'Create OSDs');
+
+ cy.get('.title').should('contain.text', 'Create OSDs');
+ });
+
+ describe('when Orchestrator is available', () => {
+ it('should create OSDs', () => {
+ osds.navigateTo();
+ osds.getTableCount('total').as('initOSDCount');
+
+ createCluster.navigateTo();
+ createCluster.createCluster();
+ cy.get('button[aria-label="Next"]').click();
+
+ createCluster.createOSD('hdd');
+
+ cy.get('button[aria-label="Next"]').click();
+ cy.get('button[aria-label="Next"]').click();
+ });
+ });
+});
createCluster.createCluster();
cy.get('button[aria-label="Next"]').click();
+ cy.get('button[aria-label="Next"]').click();
});
describe('navigation link and title test', () => {
it('should check if nav-link and title contains Review', () => {
cy.get('.nav-link').should('contain.text', 'Review');
-
- cy.get('.title').should('contain.text', 'Review');
});
});
// check for fields in table
createCluster.getStatusTables().should('contain.text', 'Hosts');
+ createCluster.getStatusTables().should('contain.text', 'Storage Capacity');
});
it('should check Hosts by Label and Host Details tables are present', () => {
import { CreateClusterWizardHelper } from 'cypress/integration/cluster/create-cluster.po';
import { HostsPageHelper } from 'cypress/integration/cluster/hosts.po';
+import { OSDsPageHelper } from 'cypress/integration/cluster/osds.po';
describe('when cluster creation is completed', () => {
const createCluster = new CreateClusterWizardHelper();
createCluster.navigateTo();
createCluster.createCluster();
+ cy.get('button[aria-label="Next"]').click();
cy.get('button[aria-label="Next"]').click();
cy.get('button[aria-label="Next"]').click();
beforeEach(() => {
hosts.navigateTo();
});
- it('should have already exited from maintenance', () => {
+ it('should have removed "_no_schedule" label', () => {
for (let host = 0; host < hostnames.length; host++) {
- cy.get('datatable-row-wrapper').should('not.have.text', 'maintenance');
+ cy.get('datatable-row-wrapper').should('not.have.text', '_no_schedule');
}
});
});
});
});
+
+ describe('OSDs page', () => {
+ const osds = new OSDsPageHelper();
+
+ beforeEach(() => {
+ osds.navigateTo();
+ });
+
+ it('should check if osds are created', { retries: 1 }, () => {
+ osds.expectTableCount('total', 2);
+ });
+ });
});
class="bold">Hosts</td>
<td>{{ hostsCount }}</td>
</tr>
+ <tr>
+ <td i18n
+ class="bold">Storage Capacity</td>
+ <td><span i18n
+ *ngIf="filteredDevices && capacity">Number of devices: {{ filteredDevices.length }}. Raw capacity:
+ {{ capacity | dimlessBinary }}.</span></td>
+ </tr>
</table>
</fieldset>
</div>
import { HostService } from '~/app/shared/api/host.service';
import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { WizardStepsService } from '~/app/shared/services/wizard-steps.service';
+import { InventoryDevice } from '../inventory/inventory-devices/inventory-device.model';
@Component({
selector: 'cd-create-cluster-review',
labelOccurrences = {};
hostsCountPerLabel: object[] = [];
uniqueLabels: Set<string> = new Set();
+ filteredDevices: InventoryDevice[] = [];
+ capacity = 0;
- constructor(private hostService: HostService) {}
+ constructor(private hostService: HostService, public wizardStepService: WizardStepsService) {}
ngOnInit() {
this.hostsDetails = {
this.hostsByLabel['data'] = [...this.hostsCountPerLabel];
this.hostsDetails['data'] = [...this.hosts];
});
+
+ this.filteredDevices = this.wizardStepService.osdDevices;
+ this.capacity = this.wizardStepService.osdCapacity;
}
}
</div>
<div *ngSwitchCase="'2'"
class="ml-5">
+ <h4 class="title"
+ i18n>Create OSDs</h4>
+ <br>
+ <div class="alignForm">
+ <cd-osd-form [clusterCreation]="true"></cd-osd-form>
+ </div>
+ </div>
+ <div *ngSwitchCase="'3'"
+ class="ml-5">
<cd-create-cluster-review></cd-create-cluster-review>
</div>
</ng-container>
display: none;
}
}
+
+.alignForm {
+ margin-left: -1%;
+}
import { CoreModule } from '~/app/core/core.module';
import { HostService } from '~/app/shared/api/host.service';
import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
+import { LoadingPanelComponent } from '~/app/shared/components/loading-panel/loading-panel.component';
import { AppConstants } from '~/app/shared/constants/app.constants';
import { ModalService } from '~/app/shared/services/modal.service';
import { WizardStepsService } from '~/app/shared/services/wizard-steps.service';
let modalServiceShowSpy: jasmine.Spy;
const projectConstants: typeof AppConstants = AppConstants;
- configureTestBed({
- imports: [
- HttpClientTestingModule,
- RouterTestingModule,
- ToastrModule.forRoot(),
- SharedModule,
- CoreModule,
- CephModule
- ]
- });
+ configureTestBed(
+ {
+ imports: [
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ToastrModule.forRoot(),
+ SharedModule,
+ CoreModule,
+ CephModule
+ ]
+ },
+ [LoadingPanelComponent]
+ );
beforeEach(() => {
fixture = TestBed.createComponent(CreateClusterComponent);
component.onNextStep();
fixture.detectChanges();
expect(wizardStepServiceSpy).toHaveBeenCalledTimes(1);
- expect(hostServiceSpy).toBeCalledTimes(2);
+ expect(hostServiceSpy).toBeCalledTimes(1);
});
it('should show the button labels correctly', () => {
let cancelBtnLabel = component.showCancelButtonLabel();
expect(cancelBtnLabel).toEqual('Cancel');
+ component.onNextStep();
+ fixture.detectChanges();
+ submitBtnLabel = component.showSubmitButtonLabel();
+ expect(submitBtnLabel).toEqual('Next');
+ cancelBtnLabel = component.showCancelButtonLabel();
+ expect(cancelBtnLabel).toEqual('Back');
+
// Last page of the wizard
component.onNextStep();
fixture.detectChanges();
-import { Component, OnDestroy, TemplateRef, ViewChild } from '@angular/core';
+import { Component, EventEmitter, OnDestroy, Output, TemplateRef, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
import { forkJoin, Subscription } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { ClusterService } from '~/app/shared/api/cluster.service';
import { HostService } from '~/app/shared/api/host.service';
+import { OsdService } from '~/app/shared/api/osd.service';
import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
-import { ActionLabelsI18n, AppConstants } from '~/app/shared/constants/app.constants';
+import { ActionLabelsI18n, AppConstants, URLVerbs } from '~/app/shared/constants/app.constants';
import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { FinishedTask } from '~/app/shared/models/finished-task';
import { Permissions } from '~/app/shared/models/permissions';
import { WizardStepModel } from '~/app/shared/models/wizard-steps';
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
import { ModalService } from '~/app/shared/services/modal.service';
import { NotificationService } from '~/app/shared/services/notification.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
import { WizardStepsService } from '~/app/shared/services/wizard-steps.service';
+import { DriveGroup } from '../osd/osd-form/drive-group.model';
@Component({
selector: 'cd-create-cluster',
currentStepSub: Subscription;
permissions: Permissions;
projectConstants: typeof AppConstants = AppConstants;
- stepTitles = ['Add Hosts', 'Review'];
+ stepTitles = ['Add Hosts', 'Create OSDs', 'Review'];
startClusterCreation = false;
observables: any = [];
modalRef: NgbModalRef;
+ driveGroup = new DriveGroup();
+ driveGroups: Object[] = [];
+
+ @Output()
+ submitAction = new EventEmitter();
constructor(
private authStorageService: AuthStorageService,
private notificationService: NotificationService,
private actionLabels: ActionLabelsI18n,
private clusterService: ClusterService,
- private modalService: ModalService
+ private modalService: ModalService,
+ private taskWrapper: TaskWrapperService,
+ private osdService: OsdService,
+ private wizardStepService: WizardStepsService
) {
this.permissions = this.authStorageService.getPermissions();
this.currentStepSub = this.stepsService.getCurrentStep().subscribe((step: WizardStepModel) => {
.subscribe({
error: (error) => error.preventDefault()
});
+
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('osd/' + URLVerbs.CREATE, {
+ tracking_id: _.join(_.map(this.driveGroups, 'service_id'), ', ')
+ }),
+ call: this.osdService.create(this.driveGroups)
+ })
+ .subscribe({
+ error: (error) => error.preventDefault(),
+ complete: () => {
+ this.submitAction.emit();
+ }
+ });
}
onNextStep() {
if (!this.stepsService.isLastStep()) {
this.hostService.list().subscribe((hosts) => {
hosts.forEach((host) => {
- if (host['status'] === 'maintenance') {
- this.observables.push(this.hostService.update(host['hostname'], false, [], true));
+ const index = host['labels'].indexOf('_no_schedule', 0);
+ if (index > -1) {
+ host['labels'].splice(index, 1);
+ this.observables.push(this.hostService.update(host['hostname'], true, host['labels']));
}
});
});
+ this.driveGroup = this.wizardStepService.sharedData;
+ this.stepsService.getCurrentStep().subscribe((step: WizardStepModel) => {
+ this.currentStep = step;
+ });
+ if (this.currentStep.stepIndex === 2 && this.driveGroup) {
+ const user = this.authStorageService.getUsername();
+ this.driveGroup.setName(`dashboard-${user}-${_.now()}`);
+ this.driveGroups.push(this.driveGroup.spec);
+ }
this.stepsService.moveToNextStep();
} else {
this.onSubmit();
hostnames: string[];
addr: string;
status: string;
- allLabels: any;
+ allLabels: string[];
pageURL: string;
messages = new SelectMessages({
}
private createForm() {
- const disableMaintenance = this.pageURL !== 'hosts';
this.hostForm = new CdFormGroup({
hostname: new FormControl('', {
validators: [
validators: [CdValidators.ip()]
}),
labels: new FormControl([]),
- maintenance: new FormControl({ value: disableMaintenance, disabled: disableMaintenance })
+ maintenance: new FormControl({ value: false, disabled: this.pageURL !== 'hosts' })
});
}
this.addr = this.hostForm.get('addr').value;
this.status = this.hostForm.get('maintenance').value ? 'maintenance' : '';
this.allLabels = this.hostForm.get('labels').value;
+ if (this.pageURL !== 'hosts' && !this.allLabels.includes('_no_schedule')) {
+ this.allLabels.push('_no_schedule');
+ }
this.taskWrapper
.wrapTaskAroundCall({
task: new FinishedTask('host/' + URLVerbs.ADD, {
icon: Icons.enter,
click: () => this.hostMaintenance(),
disable: (selection: CdTableSelection) =>
- this.getDisable('maintenance', selection) ||
- this.isExecuting ||
- this.enableButton ||
- this.clusterCreation
+ this.getDisable('maintenance', selection) || this.isExecuting || this.enableButton,
+ visible: () => !this.clusterCreation
},
{
name: this.actionLabels.EXIT_MAINTENANCE,
icon: Icons.exit,
click: () => this.hostMaintenance(),
disable: (selection: CdTableSelection) =>
- this.getDisable('maintenance', selection) ||
- this.isExecuting ||
- !this.enableButton ||
- this.clusterCreation
+ this.getDisable('maintenance', selection) || this.isExecuting || this.enableButton,
+ visible: () => !this.clusterCreation
}
];
}
this.orchService.status().subscribe((status: OrchestratorStatus) => {
this.orchStatus = status;
});
+
+ if (this.clusterCreation) {
+ const hiddenColumns = ['services', 'ceph_version'];
+ this.columns = this.columns.filter((col: any) => {
+ return !hiddenColumns.includes(col.prop);
+ });
+ }
}
updateSelection(selection: CdTableSelection) {
[forceIdentifier]="true"
[selectionType]="selectionType"
columnMode="flex"
- [autoReload]="false"
+ (fetchData)="getDevices()"
[searchField]="false"
(updateSelection)="updateSelection($event)"
(columnFiltersChanged)="onColumnFiltersChanged($event)">
import { ToastrModule } from 'ngx-toastr';
+import { HostService } from '~/app/shared/api/host.service';
import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
import { CdTableAction } from '~/app/shared/models/cd-table-action';
let component: InventoryDevicesComponent;
let fixture: ComponentFixture<InventoryDevicesComponent>;
let orchService: OrchestratorService;
+ let hostService: HostService;
const fakeAuthStorageService = {
getPermissions: () => {
beforeEach(() => {
fixture = TestBed.createComponent(InventoryDevicesComponent);
component = fixture.componentInstance;
+ hostService = TestBed.inject(HostService);
orchService = TestBed.inject(OrchestratorService);
});
expect(component.columns.every((column) => Boolean(column.prop))).toBeTruthy();
});
+ it('should call inventoryDataList only when showOnlyAvailableData is true', () => {
+ const hostServiceSpy = spyOn(hostService, 'inventoryDeviceList').and.callThrough();
+ component.getDevices();
+ expect(hostServiceSpy).toBeCalledTimes(0);
+ component.showAvailDeviceOnly = true;
+ component.getDevices();
+ expect(hostServiceSpy).toBeCalledTimes(1);
+ });
+
describe('table actions', () => {
const fakeDevices = require('./fixtures/inventory_list_response.json');
// Devices
@Input() devices: InventoryDevice[] = [];
+ @Input() showAvailDeviceOnly = false;
// Do not display these columns
@Input() hiddenColumns: string[] = [];
}
}
+ getDevices() {
+ if (this.showAvailDeviceOnly) {
+ this.hostService.inventoryDeviceList().subscribe(
+ (devices: InventoryDevice[]) => {
+ this.devices = _.filter(devices, 'available');
+ this.devices = [...this.devices];
+ },
+ () => {
+ this.devices = [];
+ }
+ );
+ } else {
+ this.devices = [...this.devices];
+ }
+ }
+
ngOnDestroy() {
if (this.fetchInventorySub) {
this.fetchInventorySub.unsubscribe();
describe('after ngOnInit', () => {
it('should load devices', () => {
fixture.detectChanges();
- expect(hostService.inventoryDeviceList).toHaveBeenNthCalledWith(1, undefined, false);
component.refresh(); // click refresh button
- expect(hostService.inventoryDeviceList).toHaveBeenNthCalledWith(2, undefined, true);
+ expect(hostService.inventoryDeviceList).toHaveBeenNthCalledWith(1, undefined, false);
const newHost = 'host0';
component.hostname = newHost;
fixture.detectChanges();
component.ngOnChanges();
- expect(hostService.inventoryDeviceList).toHaveBeenNthCalledWith(3, newHost, false);
+ expect(hostService.inventoryDeviceList).toHaveBeenNthCalledWith(2, newHost, false);
component.refresh(); // click refresh button
- expect(hostService.inventoryDeviceList).toHaveBeenNthCalledWith(4, newHost, true);
+ expect(hostService.inventoryDeviceList).toHaveBeenNthCalledWith(3, newHost, true);
});
});
});
<cd-inventory-devices #inventoryDevices
[devices]="devices"
[filterColumns]="filterColumns"
+ [showAvailDeviceOnly]="true"
[hiddenColumns]="['available', 'osd_ids']"
(filterChange)="onFilterChange($event)">
</cd-inventory-devices>
import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
import { CdTableColumnFiltersChange } from '~/app/shared/models/cd-table-column-filters-change';
+import { WizardStepsService } from '~/app/shared/services/wizard-steps.service';
@Component({
selector: 'cd-osd-devices-selection-modal',
constructor(
private formBuilder: CdFormBuilder,
public activeModal: NgbActiveModal,
- public actionLabels: ActionLabelsI18n
+ public actionLabels: ActionLabelsI18n,
+ public wizardStepService: WizardStepsService
) {
this.action = actionLabels.ADD;
this.createForm();
this.filteredDevices = event.data;
this.capacity = _.sumBy(this.filteredDevices, 'sys_api.size');
this.event = event;
+ this.wizardStepService.osdDevices = this.filteredDevices;
+ this.wizardStepService.osdCapacity = this.capacity;
}
}
[formGroup]="form"
novalidate>
<div class="card">
- <div i18n="form title"
- class="card-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+ <div i18n="form title|Example: Create Pool@@formTitle"
+ class="card-header"
+ *ngIf="!clusterCreation">{{ action | titlecase }} {{ resource | upperFirst }}</div>
<div class="card-body">
<fieldset>
<cd-osd-devices-selection-groups #dataDeviceSelectionGroups
</div>
</fieldset>
</div>
- <div class="card-footer">
+ <div class="card-footer"
+ *ngIf="!clusterCreation">
<cd-form-button-panel #previewButtonPanel
(submitActionEvent)="submit()"
[form]="form"
-import { Component, OnInit, ViewChild } from '@angular/core';
+import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { CdTableColumn } from '~/app/shared/models/cd-table-column';
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
import { ModalService } from '~/app/shared/services/modal.service';
+import { WizardStepsService } from '~/app/shared/services/wizard-steps.service';
import { OsdCreationPreviewModalComponent } from '../osd-creation-preview-modal/osd-creation-preview-modal.component';
import { DevicesSelectionChangeEvent } from '../osd-devices-selection-groups/devices-selection-change-event.interface';
import { DevicesSelectionClearEvent } from '../osd-devices-selection-groups/devices-selection-clear-event.interface';
featureList: OsdFeature[] = [];
hasOrchestrator = true;
+ @Input()
+ clusterCreation = false;
constructor(
public actionLabels: ActionLabelsI18n,
private orchService: OrchestratorService,
private hostService: HostService,
private router: Router,
- private modalService: ModalService
+ private modalService: ModalService,
+ public wizardStepService: WizardStepsService
) {
super();
this.resource = $localize`OSDs`;
this.enableFeatures();
}
this.driveGroup.setDeviceSelection(event.type, event.filters);
+ if (this.clusterCreation) {
+ this.wizardStepService.sharedData = this.driveGroup;
+ }
}
onDevicesCleared(event: DevicesSelectionClearEvent) {
import { BehaviorSubject, Observable } from 'rxjs';
+import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
+import { DriveGroup } from '~/app/ceph/cluster/osd/osd-form/drive-group.model';
import { WizardStepModel } from '~/app/shared/models/wizard-steps';
const initialStep = [{ stepIndex: 1, isComplete: false }];
export class WizardStepsService {
steps$: BehaviorSubject<WizardStepModel[]>;
currentStep$: BehaviorSubject<WizardStepModel> = new BehaviorSubject<WizardStepModel>(null);
+ sharedData = new DriveGroup();
+ osdDevices: InventoryDevice[] = [];
+ osdCapacity = 0;
constructor() {
this.steps$ = new BehaviorSubject<WizardStepModel[]>(initialStep);