From: Nizamudeen A Date: Tue, 22 Feb 2022 10:21:03 +0000 (+0530) Subject: mgr/dashboard: OSD Creation Workflow initial works X-Git-Tag: v17.2.6~130^2~78^2~2 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=b350266d9758c29e559849c2527bbd3d5cbb0a22;p=ceph.git mgr/dashboard: OSD Creation Workflow initial works Introducing the Cost/Capacity Optimized deployment option Used bootstrap accordion Adapted the e2e but not written new tests for the deployment option Fixes: https://tracker.ceph.com/issues/54340 Fixes: https://tracker.ceph.com/issues/54563 Signed-off-by: Nizamudeen A Signed-off-by: Sarthak0702 (cherry picked from commit 6c2dcb740efb793a3f6ef593793151a34c19ca01) --- diff --git a/src/pybind/mgr/dashboard/controllers/osd.py b/src/pybind/mgr/dashboard/controllers/osd.py index ea12a842ea4bf..d9b91ba3ce176 100644 --- a/src/pybind/mgr/dashboard/controllers/osd.py +++ b/src/pybind/mgr/dashboard/controllers/osd.py @@ -14,6 +14,7 @@ from ..security import Scope from ..services.ceph_service import CephService, SendCommandError from ..services.exception import handle_orchestrator_error, handle_send_command_error from ..services.orchestrator import OrchClient, OrchFeature +from ..services.osd import HostStorageSummary, OsdDeploymentOptions from ..tools import str_to_bool from . import APIDoc, APIRouter, CreatePermission, DeletePermission, Endpoint, \ EndpointDoc, ReadPermission, RESTController, Task, UIRouter, \ @@ -47,35 +48,22 @@ EXPORT_INDIV_FLAGS_GET_SCHEMA = { } -class DeploymentOption: - def __init__(self, name: str, available=False, capacity=0, used=0, hdd_used=0, - ssd_used=0, nvme_used=0): - self.name = name - self.available = available - self.capacity = capacity - self.used = used - self.hdd_used = hdd_used - self.ssd_used = ssd_used - self.nvme_used = nvme_used - - def as_dict(self): - return { - 'name': self.name, - 'available': self.available, - 'capacity': self.capacity, - 'used': self.used, - 'hdd_used': self.hdd_used, - 'ssd_used': self.ssd_used, - 'nvme_used': self.nvme_used - } - - class DeploymentOptions: def __init__(self): self.options = { - 'cost-capacity': DeploymentOption('cost-capacity'), - 'throughput': DeploymentOption('throughput-optimized'), - 'iops': DeploymentOption('iops-optimized'), + OsdDeploymentOptions.COST_CAPACITY: + HostStorageSummary(OsdDeploymentOptions.COST_CAPACITY, + title='Cost/Capacity-optimized', + desc='All the available HDDs are selected'), + OsdDeploymentOptions.THROUGHPUT: + HostStorageSummary(OsdDeploymentOptions.THROUGHPUT, + title='Throughput-optimized', + desc="HDDs/SSDs are selected for data" + "devices and SSDs/NVMes for DB/WAL devices"), + OsdDeploymentOptions.IOPS: + HostStorageSummary(OsdDeploymentOptions.IOPS, + title='IOPS-optimized', + desc='All the available NVMes are selected'), } self.recommended_option = None @@ -87,17 +75,19 @@ class DeploymentOptions: predefined_drive_groups = { - 'cost-capacity': { + OsdDeploymentOptions.COST_CAPACITY: { 'service_type': 'osd', + 'service_id': 'cost_capacity', 'placement': { 'host_pattern': '*' }, 'data_devices': { 'rotational': 1 - } + }, + 'encrypted': False }, - 'throughput': {}, - 'iops': {}, + OsdDeploymentOptions.THROUGHPUT: {}, + OsdDeploymentOptions.IOPS: {}, } @@ -347,10 +337,12 @@ class Osd(RESTController): def _create_predefined_drive_group(self, data): orch = OrchClient.instance() - if data == 'cost-capacity': + if OsdDeploymentOptions(data[0]['option']) == OsdDeploymentOptions.COST_CAPACITY: try: + predefined_drive_groups[ + OsdDeploymentOptions.COST_CAPACITY]['encrypted'] = data[0]['encrypted'] orch.osds.create([DriveGroupSpec.from_json( - predefined_drive_groups['cost-capacity'])]) + predefined_drive_groups[OsdDeploymentOptions.COST_CAPACITY])]) except (ValueError, TypeError, DriveGroupValidationError) as e: raise DashboardException(e, component='osd') @@ -473,7 +465,7 @@ class Osd(RESTController): @UIRouter('/osd', Scope.OSD) @APIDoc("Dashboard UI helper function; not part of the public API", "OsdUI") class OsdUi(Osd): - @Endpoint('GET', version=APIVersion.EXPERIMENTAL) + @Endpoint('GET') @ReadPermission @raise_if_no_orchestrator([OrchFeature.DAEMON_LIST]) @handle_orchestrator_error('host') @@ -484,6 +476,7 @@ class OsdUi(Osd): nvmes = 0 res = DeploymentOptions() devices = {} + for inventory_host in orch.inventory.list(hosts=None, refresh=True): for device in inventory_host.devices.devices: if device.available: @@ -495,8 +488,8 @@ class OsdUi(Osd): elif device.human_readable_type == 'nvme': nvmes += 1 if hdds: - res.options['cost-capacity'].available = True - res.recommended_option = 'cost-capacity' + res.options[OsdDeploymentOptions.COST_CAPACITY].available = True + res.recommended_option = OsdDeploymentOptions.COST_CAPACITY return res.as_dict() diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.po.ts index d388a3c5ba6e6..cd812f474fb89 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.po.ts @@ -14,6 +14,7 @@ export class OSDsPageHelper extends PageHelper { }; create(deviceType: 'hdd' | 'ssd', hostname?: string, expandCluster = false) { + cy.get('[aria-label="toggle advanced mode"]').click(); // Click Primary devices Add button cy.get('cd-osd-devices-selection-groups[name="Primary"]').as('primaryGroups'); cy.get('@primaryGroups').find('button').click(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html index 4a4bb109472f8..4808773727b1c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html @@ -50,11 +50,12 @@ class="ml-5">

Create OSDs

-
+ (emitDriveGroup)="setDriveGroup($event)" + (emitDeploymentOption)="setDeploymentOptions($event)" + (emitMode)="setDeploymentMode($event)">
{ }); it('should ensure osd creation did not happen when no devices are selected', () => { + component.simpleDeployment = false; const osdServiceSpy = spyOn(osdService, 'create').and.callThrough(); component.onSubmit(); fixture.detectChanges(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.ts index 743902b712d8c..02333c39bf617 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.ts @@ -1,4 +1,12 @@ -import { Component, EventEmitter, OnDestroy, Output, TemplateRef, ViewChild } from '@angular/core'; +import { + Component, + EventEmitter, + OnDestroy, + OnInit, + Output, + TemplateRef, + ViewChild +} from '@angular/core'; import { Router } from '@angular/router'; import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; @@ -13,6 +21,7 @@ import { ConfirmationModalComponent } from '~/app/shared/components/confirmation 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 { DeploymentOptions } from '~/app/shared/models/osd-deployment-options'; import { Permissions } from '~/app/shared/models/permissions'; import { WizardStepModel } from '~/app/shared/models/wizard-steps'; import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; @@ -27,7 +36,7 @@ import { DriveGroup } from '../osd/osd-form/drive-group.model'; templateUrl: './create-cluster.component.html', styleUrls: ['./create-cluster.component.scss'] }) -export class CreateClusterComponent implements OnDestroy { +export class CreateClusterComponent implements OnInit, OnDestroy { @ViewChild('skipConfirmTpl', { static: true }) skipConfirmTpl: TemplateRef; currentStep: WizardStepModel; @@ -40,6 +49,9 @@ export class CreateClusterComponent implements OnDestroy { modalRef: NgbModalRef; driveGroup = new DriveGroup(); driveGroups: Object[] = []; + deploymentOption: DeploymentOptions; + selectedOption = {}; + simpleDeployment = true; @Output() submitAction = new EventEmitter(); @@ -65,6 +77,13 @@ export class CreateClusterComponent implements OnDestroy { this.currentStep.stepIndex = 1; } + ngOnInit(): void { + this.osdService.getDeploymentOptions().subscribe((options) => { + this.deploymentOption = options; + this.selectedOption = { option: options.recommended_option }; + }); + } + createCluster() { this.startClusterCreation = true; } @@ -118,34 +137,63 @@ export class CreateClusterComponent implements OnDestroy { error: (error) => error.preventDefault() }); }); + if (this.driveGroup) { const user = this.authStorageService.getUsername(); this.driveGroup.setName(`dashboard-${user}-${_.now()}`); this.driveGroups.push(this.driveGroup.spec); } - if (this.osdService.osdDevices['totalDevices'] > 0) { + if (this.simpleDeployment) { + const title = this.deploymentOption?.options[this.selectedOption['option']].title; + const trackingId = $localize`${title} deployment`; this.taskWrapper .wrapTaskAroundCall({ task: new FinishedTask('osd/' + URLVerbs.CREATE, { - tracking_id: _.join(_.map(this.driveGroups, 'service_id'), ', ') + tracking_id: trackingId }), - call: this.osdService.create(this.driveGroups) + call: this.osdService.create([this.selectedOption], trackingId, 'predefined') }) .subscribe({ error: (error) => error.preventDefault(), complete: () => { this.submitAction.emit(); - this.osdService.osdDevices = []; } }); + } else { + if (this.osdService.osdDevices['totalDevices'] > 0) { + this.driveGroup.setFeature('encrypted', this.selectedOption['encrypted']); + const trackingId = _.join(_.map(this.driveGroups, 'service_id'), ', '); + this.taskWrapper + .wrapTaskAroundCall({ + task: new FinishedTask('osd/' + URLVerbs.CREATE, { + tracking_id: trackingId + }), + call: this.osdService.create(this.driveGroups, trackingId) + }) + .subscribe({ + error: (error) => error.preventDefault(), + complete: () => { + this.submitAction.emit(); + this.osdService.osdDevices = []; + } + }); + } } } - getDriveGroup(driveGroup: DriveGroup) { + setDriveGroup(driveGroup: DriveGroup) { this.driveGroup = driveGroup; } + setDeploymentOptions(option: object) { + this.selectedOption = option; + } + + setDeploymentMode(mode: boolean) { + this.simpleDeployment = mode; + } + onNextStep() { if (!this.wizardStepsService.isLastStep()) { this.wizardStepsService.getCurrentStep().subscribe((step: WizardStepModel) => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.ts index 979dcc3411fb5..3e1b0f067c47a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.ts @@ -41,12 +41,13 @@ export class OsdCreationPreviewModalComponent { } onSubmit() { + const trackingId = _.join(_.map(this.driveGroups, 'service_id'), ', '); this.taskWrapper .wrapTaskAroundCall({ task: new FinishedTask('osd/' + URLVerbs.CREATE, { - tracking_id: _.join(_.map(this.driveGroups, 'service_id'), ', ') + tracking_id: trackingId }), - call: this.osdService.create(this.driveGroups) + call: this.osdService.create(this.driveGroups, trackingId) }) .subscribe({ error: () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.html index 59a17362f6f83..d4b6d9faea109 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.html @@ -1,136 +1,213 @@ -
-
-
-
{{ action | titlecase }} {{ resource | upperFirst }}
-
-
- - -
+
{{ action | titlecase }} {{ resource | upperFirst }}
+
+ +
+
+
+

+ +

+
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+

+ +

+
+
+
+
+
+
+ + +
- -
- Shared devices + +
+ Shared devices - - - + + + - -
- -
- - Value should be greater than or equal to 0 -
-
+ +
+ +
+ + Value should be greater than or equal to 0 +
+
- - - + + + - -
- -
- - Value should be greater than or equal to 0 + +
+ +
+ + Value should be greater than or equal to 0 +
+
+
- - - -
- Configuration +
- -
- -
+ +
+
+

+ +

+
+
+
+
+
+ formControlName="{{ feature.key }}" + (change)="emitDeploymentSelection()">
- -
-
-
- + +
+ +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.spec.ts index 2044b084c7aac..725fc953fbb6c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.spec.ts @@ -9,9 +9,14 @@ import { BehaviorSubject, of } from 'rxjs'; import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model'; import { InventoryDevicesComponent } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-devices.component'; +import { DashboardModule } from '~/app/ceph/dashboard/dashboard.module'; import { HostService } from '~/app/shared/api/host.service'; import { OrchestratorService } from '~/app/shared/api/orchestrator.service'; import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { + DeploymentOptions, + OsdDeploymentOptions +} from '~/app/shared/models/osd-deployment-options'; import { SummaryService } from '~/app/shared/services/summary.service'; import { SharedModule } from '~/app/shared/shared.module'; import { configureTestBed, FixtureHelper, FormHelper } from '~/testing/unit-test-helper'; @@ -50,6 +55,45 @@ describe('OsdFormComponent', () => { } ]; + const deploymentOptions: DeploymentOptions = { + options: { + cost_capacity: { + name: OsdDeploymentOptions.COST_CAPACITY, + available: true, + capacity: 0, + used: 0, + hdd_used: 0, + ssd_used: 0, + nvme_used: 0, + title: 'Cost/Capacity-optimized', + desc: 'All the available HDDs are selected' + }, + throughput_optimized: { + name: OsdDeploymentOptions.THROUGHPUT, + available: false, + capacity: 0, + used: 0, + hdd_used: 0, + ssd_used: 0, + nvme_used: 0, + title: 'Throughput-optimized', + desc: 'HDDs/SSDs are selected for data devices and SSDs/NVMes for DB/WAL devices' + }, + iops_optimized: { + name: OsdDeploymentOptions.IOPS, + available: false, + capacity: 0, + used: 0, + hdd_used: 0, + ssd_used: 0, + nvme_used: 0, + title: 'IOPS-optimized', + desc: 'All the available NVMes are selected' + } + }, + recommended_option: OsdDeploymentOptions.COST_CAPACITY + }; + const expectPreviewButton = (enabled: boolean) => { const debugElement = fixtureHelper.getElementByCss('.tc_submitButton'); expect(debugElement.nativeElement.disabled).toBe(!enabled); @@ -99,7 +143,8 @@ describe('OsdFormComponent', () => { SharedModule, RouterTestingModule, ReactiveFormsModule, - ToastrModule.forRoot() + ToastrModule.forRoot(), + DashboardModule ], declarations: [OsdFormComponent, OsdDevicesSelectionGroupsComponent, InventoryDevicesComponent] }); @@ -141,14 +186,53 @@ describe('OsdFormComponent', () => { describe('with orchestrator', () => { beforeEach(() => { + component.simpleDeployment = false; spyOn(orchService, 'status').and.returnValue(of({ available: true })); spyOn(hostService, 'inventoryDeviceList').and.returnValue(of([])); + component.deploymentOptions = deploymentOptions; + fixture.detectChanges(); + }); + + it('should display the accordion', () => { + fixtureHelper.expectElementVisible('.card-body .accordion', true); + }); + + it('should display the three deployment scenarios', () => { + fixtureHelper.expectElementVisible('#cost_capacity', true); + fixtureHelper.expectElementVisible('#throughput_optimized', true); + fixtureHelper.expectElementVisible('#iops_optimized', true); + }); + + it('should only disable the options that are not available', () => { + let radioBtn = fixtureHelper.getElementByCss('#throughput_optimized').nativeElement; + expect(radioBtn.disabled).toBeTruthy(); + radioBtn = fixtureHelper.getElementByCss('#iops_optimized').nativeElement; + expect(radioBtn.disabled).toBeTruthy(); + + // Make the throughput_optimized option available and verify the option is not disabled + deploymentOptions.options['throughput_optimized'].available = true; + fixture.detectChanges(); + radioBtn = fixtureHelper.getElementByCss('#throughput_optimized').nativeElement; + expect(radioBtn.disabled).toBeFalsy(); + }); + + it('should be a Recommended option only when it is recommended by backend', () => { + const label = fixtureHelper.getElementByCss('#label_cost_capacity').nativeElement; + const throughputLabel = fixtureHelper.getElementByCss('#label_throughput_optimized') + .nativeElement; + + expect(label.innerHTML).toContain('Recommended'); + expect(throughputLabel.innerHTML).not.toContain('Recommended'); + + deploymentOptions.recommended_option = OsdDeploymentOptions.THROUGHPUT; fixture.detectChanges(); + expect(throughputLabel.innerHTML).toContain('Recommended'); + expect(label.innerHTML).not.toContain('Recommended'); }); it('should display form', () => { fixtureHelper.expectElementVisible('cd-alert-panel', false); - fixtureHelper.expectElementVisible('.cd-col-form form', true); + fixtureHelper.expectElementVisible('.card-body form', true); }); describe('without data devices selected', () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.ts index 71ca2d8f7b2ea..c2384425e7019 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.ts @@ -7,15 +7,21 @@ import _ from 'lodash'; import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model'; import { HostService } from '~/app/shared/api/host.service'; import { OrchestratorService } from '~/app/shared/api/orchestrator.service'; +import { OsdService } from '~/app/shared/api/osd.service'; import { FormButtonPanelComponent } from '~/app/shared/components/form-button-panel/form-button-panel.component'; -import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; import { Icons } from '~/app/shared/enum/icons.enum'; import { CdForm } from '~/app/shared/forms/cd-form'; import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; import { CdTableColumn } from '~/app/shared/models/cd-table-column'; +import { FinishedTask } from '~/app/shared/models/finished-task'; +import { + DeploymentOptions, + OsdDeploymentOptions +} from '~/app/shared/models/osd-deployment-options'; 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 { TaskWrapperService } from '~/app/shared/services/task-wrapper.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'; @@ -49,6 +55,10 @@ export class OsdFormComponent extends CdForm implements OnInit { @Output() emitDriveGroup: EventEmitter = new EventEmitter(); + @Output() emitDeploymentOption: EventEmitter = new EventEmitter(); + + @Output() emitMode: EventEmitter = new EventEmitter(); + icons = Icons; form: CdFormGroup; @@ -71,6 +81,11 @@ export class OsdFormComponent extends CdForm implements OnInit { hasOrchestrator = true; + simpleDeployment = true; + + deploymentOptions: DeploymentOptions; + optionNames = Object.values(OsdDeploymentOptions); + constructor( public actionLabels: ActionLabelsI18n, private authStorageService: AuthStorageService, @@ -78,7 +93,8 @@ export class OsdFormComponent extends CdForm implements OnInit { private hostService: HostService, private router: Router, private modalService: ModalService, - public wizardStepService: WizardStepsService + private osdService: OsdService, + private taskWrapper: TaskWrapperService ) { super(); this.resource = $localize`OSDs`; @@ -103,6 +119,14 @@ export class OsdFormComponent extends CdForm implements OnInit { } }); + this.osdService.getDeploymentOptions().subscribe((options) => { + this.deploymentOptions = options; + this.form.get('deploymentOption').setValue(this.deploymentOptions?.recommended_option); + + if (this.deploymentOptions?.recommended_option) { + this.enableFeatures(); + } + }); this.form.get('walSlots').valueChanges.subscribe((value) => this.setSlots('wal', value)); this.form.get('dbSlots').valueChanges.subscribe((value) => this.setSlots('db', value)); _.each(this.features, (feature) => { @@ -123,7 +147,8 @@ export class OsdFormComponent extends CdForm implements OnInit { acc[e.key] = new FormControl({ value: false, disabled: true }); return acc; }, {}) - ) + ), + deploymentOption: new FormControl(0) }); } @@ -209,16 +234,52 @@ export class OsdFormComponent extends CdForm implements OnInit { } } + emitDeploymentSelection() { + const option = this.form.get('deploymentOption').value; + const encrypted = this.form.get('encrypted').value; + this.emitDeploymentOption.emit({ option: option, encrypted: encrypted }); + } + + emitDeploymentMode() { + this.simpleDeployment = !this.simpleDeployment; + if (!this.simpleDeployment && this.dataDeviceSelectionGroups.devices.length === 0) { + this.disableFeatures(); + } else { + this.enableFeatures(); + } + this.emitMode.emit(this.simpleDeployment); + } + submit() { - // use user name and timestamp for drive group name - const user = this.authStorageService.getUsername(); - this.driveGroup.setName(`dashboard-${user}-${_.now()}`); - const modalRef = this.modalService.show(OsdCreationPreviewModalComponent, { - driveGroups: [this.driveGroup.spec] - }); - modalRef.componentInstance.submitAction.subscribe(() => { - this.router.navigate(['/osd']); - }); - this.previewButtonPanel.submitButton.loading = false; + if (this.simpleDeployment) { + const option = this.form.get('deploymentOption').value; + const encrypted = this.form.get('encrypted').value; + const deploymentSpec = { option: option, encrypted: encrypted }; + const title = this.deploymentOptions.options[deploymentSpec.option].title; + const trackingId = `${title} deployment`; + this.taskWrapper + .wrapTaskAroundCall({ + task: new FinishedTask('osd/' + URLVerbs.CREATE, { + tracking_id: trackingId + }), + call: this.osdService.create([deploymentSpec], trackingId, 'predefined') + }) + .subscribe({ + complete: () => { + this.router.navigate(['/osd']); + } + }); + } else { + // use user name and timestamp for drive group name + const user = this.authStorageService.getUsername(); + this.driveGroup.setName(`dashboard-${user}-${_.now()}`); + const modalRef = this.modalService.show(OsdCreationPreviewModalComponent, { + driveGroups: [this.driveGroup.spec] + }); + modalRef.componentInstance.submitAction.subscribe(() => { + this.router.navigate(['/osd']); + }); + this.previewButtonPanel.submitButton.loading = false; + } } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.spec.ts index 135bbaf39bc3b..d1f9997791ae0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.spec.ts @@ -27,6 +27,7 @@ describe('OsdService', () => { }); it('should call create', () => { + const trackingId = 'all_hdd, host1_ssd'; const post_data = { method: 'drive_groups', data: [ @@ -47,9 +48,9 @@ describe('OsdService', () => { } } ], - tracking_id: 'all_hdd, host1_ssd' + tracking_id: trackingId }; - service.create(post_data.data).subscribe(); + service.create(post_data.data, trackingId).subscribe(); const req = httpTesting.expectOne('api/osd'); expect(req.request.method).toBe('POST'); expect(req.request.body).toEqual(post_data); @@ -173,4 +174,10 @@ describe('OsdService', () => { const req = httpTesting.expectOne('api/osd/1/devices'); expect(req.request.method).toBe('GET'); }); + + it('should call getDeploymentOptions', () => { + service.getDeploymentOptions().subscribe(); + const req = httpTesting.expectOne('ui-api/osd/deployment_options'); + expect(req.request.method).toBe('GET'); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts index c8f881d5e13f5..10a0cf47f0887 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts @@ -7,6 +7,7 @@ import { map } from 'rxjs/operators'; import { CdDevice } from '../models/devices'; import { InventoryDeviceType } from '../models/inventory-device-type.model'; +import { DeploymentOptions } from '../models/osd-deployment-options'; import { OsdSettings } from '../models/osd-settings'; import { SmartDataResponseV1 } from '../models/smart'; import { DeviceService } from '../services/device.service'; @@ -16,6 +17,8 @@ import { DeviceService } from '../services/device.service'; }) export class OsdService { private path = 'api/osd'; + private uiPath = 'ui-api/osd'; + osdDevices: InventoryDeviceType[] = []; osdRecvSpeedModalPriorities = { @@ -65,11 +68,11 @@ export class OsdService { constructor(private http: HttpClient, private deviceService: DeviceService) {} - create(driveGroups: Object[]) { + create(driveGroups: Object[], trackingId: string, method = 'drive_groups') { const request = { - method: 'drive_groups', + method: method, data: driveGroups, - tracking_id: _.join(_.map(driveGroups, 'service_id'), ', ') + tracking_id: trackingId }; return this.http.post(this.path, request, { observe: 'response' }); } @@ -104,6 +107,10 @@ export class OsdService { return this.http.post(`${this.path}/${id}/scrub?deep=${deep}`, null); } + getDeploymentOptions() { + return this.http.get(`${this.uiPath}/deployment_options`); + } + getFlags() { return this.http.get(`${this.path}/flags`); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.scss index 80e3550cd68a5..071b02e4a9d94 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/wizard/wizard.component.scss @@ -1,5 +1,9 @@ @use './src/styles/vendor/variables' as vv; +::ng-deep cd-wizard { + width: 15%; +} + .card-body { padding-left: 0; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/osd-deployment-options.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/osd-deployment-options.ts new file mode 100644 index 0000000000000..cae869efe17ea --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/osd-deployment-options.ts @@ -0,0 +1,24 @@ +export enum OsdDeploymentOptions { + COST_CAPACITY = 'cost_capacity', + THROUGHPUT = 'throughput_optimized', + IOPS = 'iops_optimized' +} + +export interface DeploymentOption { + name: OsdDeploymentOptions; + title: string; + desc: string; + capacity: number; + available: boolean; + hdd_used: number; + used: number; + nvme_used: number; + ssd_used: number; +} + +export interface DeploymentOptions { + options: { + [key in OsdDeploymentOptions]: DeploymentOption; + }; + recommended_option: OsdDeploymentOptions; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_basics.scss b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_basics.scss index 9ff41c6242351..7a618f704fc1e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_basics.scss +++ b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_basics.scss @@ -102,3 +102,42 @@ mark { .border-success { border-left: 4px solid vv.$success; } + +.vertical-line { + border-left: 1px solid vv.$gray-400; +} + +.accordion { + .card { + border: 0; + } + + .card-header { + border: 0; + border-bottom: 3px solid vv.$white; + padding-left: 0; + + .btn:focus, + .btn.focus { + box-shadow: none; + } + + button.dropdown-toggle { + position: relative; + + &::after { + border: 0; + content: '\f054'; + font-family: 'ForkAwesome'; + font-size: 1rem; + position: absolute; + right: 20px; + transition: transform 0.3s ease-in-out; + } + + &[aria-expanded='true']::after { + transform: rotate(90deg); + } + } + } +} diff --git a/src/pybind/mgr/dashboard/services/osd.py b/src/pybind/mgr/dashboard/services/osd.py new file mode 100644 index 0000000000000..12db733cc987d --- /dev/null +++ b/src/pybind/mgr/dashboard/services/osd.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from enum import Enum + + +class OsdDeploymentOptions(str, Enum): + COST_CAPACITY = 'cost_capacity' + THROUGHPUT = 'throughput_optimized' + IOPS = 'iops_optimized' + + +class HostStorageSummary: + def __init__(self, name: str, title=None, desc=None, available=False, + capacity=0, used=0, hdd_used=0, ssd_used=0, nvme_used=0): + self.name = name + self.title = title + self.desc = desc + self.available = available + self.capacity = capacity + self.used = used + self.hdd_used = hdd_used + self.ssd_used = ssd_used + self.nvme_used = nvme_used + + def as_dict(self): + return self.__dict__ diff --git a/src/pybind/mgr/dashboard/tests/test_osd.py b/src/pybind/mgr/dashboard/tests/test_osd.py index 33b7ebcaea0d8..dce77db0d2624 100644 --- a/src/pybind/mgr/dashboard/tests/test_osd.py +++ b/src/pybind/mgr/dashboard/tests/test_osd.py @@ -5,11 +5,11 @@ from typing import Any, Dict, List, Optional from unittest import mock from ceph.deployment.drive_group import DeviceSelection, DriveGroupSpec # type: ignore -from ceph.deployment.service_spec import PlacementSpec # type: ignore +from ceph.deployment.service_spec import PlacementSpec from .. import mgr -from ..controllers._version import APIVersion from ..controllers.osd import Osd, OsdUi +from ..services.osd import OsdDeploymentOptions from ..tests import ControllerTestCase from ..tools import NotificationQueue, TaskManager from .helper import update_dict # pylint: disable=import-error @@ -404,19 +404,19 @@ class OsdTest(ControllerTestCase): ] inventory_host = create_invetory_host(devices_data) fake_client.inventory.list.return_value = [inventory_host] - self._get('/ui-api/osd/deployment_options', version=APIVersion(0, 1)) + self._get('/ui-api/osd/deployment_options') self.assertStatus(200) res = self.json_body() - self.assertTrue(res['options']['cost-capacity']['available']) - assert res['recommended_option'] == 'cost-capacity' + self.assertTrue(res['options'][OsdDeploymentOptions.COST_CAPACITY]['available']) + assert res['recommended_option'] == OsdDeploymentOptions.COST_CAPACITY for data in devices_data: data['type'] = 'ssd' inventory_host = create_invetory_host(devices_data) fake_client.inventory.list.return_value = [inventory_host] - self._get('/ui-api/osd/deployment_options', version=APIVersion(0, 1)) + self._get('/ui-api/osd/deployment_options') self.assertStatus(200) res = self.json_body() - self.assertFalse(res['options']['cost-capacity']['available']) + self.assertFalse(res['options'][OsdDeploymentOptions.COST_CAPACITY]['available']) self.assertIsNone(res['recommended_option'])