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, \
}
-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
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: {},
}
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')
@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')
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:
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()
};
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();
class="ml-5">
<h4 class="title"
i18n>Create OSDs</h4>
- <br>
<div class="alignForm">
<cd-osd-form [hideTitle]="true"
[hideSubmitBtn]="true"
- (emitDriveGroup)="getDriveGroup($event)"></cd-osd-form>
+ (emitDriveGroup)="setDriveGroup($event)"
+ (emitDeploymentOption)="setDeploymentOptions($event)"
+ (emitMode)="setDeploymentMode($event)"></cd-osd-form>
</div>
</div>
<div *ngSwitchCase="'3'"
-@use './src/styles/vendor/variables' as vv;
-
.container-fluid {
align-items: flex-start;
display: flex;
width: 100%;
}
-.card-body {
- max-width: 85%;
-}
-
-.vertical-line {
- border-left: 1px solid vv.$gray-400;
-}
-
-cd-wizard {
- width: 15%;
-}
-
cd-hosts {
::ng-deep .nav {
display: none;
}
}
-.alignForm {
- margin-left: -1%;
+cd-osd-form {
+ ::ng-deep .card {
+ border: 0;
+ }
+
+ ::ng-deep .accordion {
+ margin-left: -1.5rem;
+ }
}
});
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();
-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';
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';
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<any>;
currentStep: WizardStepModel;
modalRef: NgbModalRef;
driveGroup = new DriveGroup();
driveGroups: Object[] = [];
+ deploymentOption: DeploymentOptions;
+ selectedOption = {};
+ simpleDeployment = true;
@Output()
submitAction = new EventEmitter();
this.currentStep.stepIndex = 1;
}
+ ngOnInit(): void {
+ this.osdService.getDeploymentOptions().subscribe((options) => {
+ this.deploymentOption = options;
+ this.selectedOption = { option: options.recommended_option };
+ });
+ }
+
createCluster() {
this.startClusterCreation = true;
}
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) => {
}
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: () => {
<cd-orchestrator-doc-panel *ngIf="!hasOrchestrator"></cd-orchestrator-doc-panel>
-<div class="cd-col-form"
+<div class="card"
*cdFormLoading="loading">
- <form name="form"
- #formDir="ngForm"
- [formGroup]="form"
- novalidate>
- <div class="card">
- <div i18n="form title|Example: Create Pool@@formTitle"
- class="card-header"
- *ngIf="!hideTitle">{{ action | titlecase }} {{ resource | upperFirst }}</div>
- <div class="card-body">
- <fieldset>
- <cd-osd-devices-selection-groups #dataDeviceSelectionGroups
- name="Primary"
- type="data"
- [availDevices]="availDevices"
- [canSelect]="availDevices.length !== 0"
- (selected)="onDevicesSelected($event)"
- (cleared)="onDevicesCleared($event)">
- </cd-osd-devices-selection-groups>
- </fieldset>
+ <div i18n="form title|Example: Create Pool@@formTitle"
+ class="card-header"
+ *ngIf="!hideTitle">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+ <div class="card-body ml-2">
+ <form name="form"
+ #formDir="ngForm"
+ [formGroup]="form"
+ novalidate>
+ <div class="accordion">
+ <div class="card">
+ <div class="card-header">
+ <h2 class="mb-0">
+ <button class="btn btn-link btn-block text-left dropdown-toggle"
+ data-toggle="collapse"
+ aria-label="toggle deployment options"
+ [attr.aria-expanded]="simpleDeployment"
+ (click)="emitDeploymentMode()"
+ i18n>Deployment Options</button>
+ </h2>
+ </div>
+ </div>
+ <div class="collapse"
+ [ngClass]="{show: simpleDeployment}">
+ <div class="card-body d-flex flex-column">
+ <div class="pt-3 pb-3"
+ *ngFor="let optionName of optionNames">
+ <div class="custom-control custom-radio custom-control-inline">
+ <input class="custom-control-input"
+ type="radio"
+ name="deploymentOption"
+ [id]="optionName"
+ [value]="optionName"
+ formControlName="deploymentOption"
+ (change)="emitDeploymentSelection()"
+ [attr.disabled]="!deploymentOptions?.options[optionName].available ? true : null">
+ <label class="custom-control-label"
+ [id]="'label_' + optionName"
+ [for]="optionName"
+ i18n>{{ deploymentOptions?.options[optionName].title }}
+ {{ deploymentOptions.recommended_option === optionName ? "(Recommended)" : "" }}
+ <cd-helper>
+ <span>{{ deploymentOptions?.options[optionName].desc }}</span>
+ </cd-helper>
+ </label>
+ </div>
+ </div>
+ <!-- @TODO: Visualize the storage used on a chart -->
+ <!-- <div class="pie-chart">
+ <h4 class="text-center">Selected Capacity</h4>
+ <h5 class="margin text-center">10 Hosts | 30 NVMes </h5>
+ <div class="char-i-contain">
+ <cd-health-pie [data]="data"
+ [config]="rawCapacityChartConfig"
+ [isBytesData]="true"
+ (prepareFn)="prepareRawUsage($event[0], $event[1])">
+ </cd-health-pie>
+ </div>
+ </div> -->
+ </div>
+ </div>
+ <div class="card">
+ <div class="card-header">
+ <h2 class="mb-0">
+ <button class="btn btn-link btn-block text-left dropdown-toggle"
+ data-toggle="collapse"
+ aria-label="toggle advanced mode"
+ [attr.aria-expanded]="!simpleDeployment"
+ (click)="emitDeploymentMode()"
+ i18n>Advanced Mode</button>
+ </h2>
+ </div>
+ </div>
+ <div class="collapse"
+ [ngClass]="{show: !simpleDeployment}">
+ <div class="card-body">
+ <div class="card-body">
+ <fieldset>
+ <cd-osd-devices-selection-groups #dataDeviceSelectionGroups
+ name="Primary"
+ type="data"
+ [availDevices]="availDevices"
+ [canSelect]="availDevices.length !== 0"
+ (selected)="onDevicesSelected($event)"
+ (cleared)="onDevicesCleared($event)">
+ </cd-osd-devices-selection-groups>
+ </fieldset>
- <!-- Shared devices -->
- <fieldset>
- <legend i18n>Shared devices</legend>
+ <!-- Shared devices -->
+ <fieldset>
+ <legend i18n>Shared devices</legend>
- <!-- WAL devices button and table -->
- <cd-osd-devices-selection-groups #walDeviceSelectionGroups
- name="WAL"
- type="wal"
- [availDevices]="availDevices"
- [canSelect]="dataDeviceSelectionGroups.devices.length !== 0"
- (selected)="onDevicesSelected($event)"
- (cleared)="onDevicesCleared($event)">
- </cd-osd-devices-selection-groups>
+ <!-- WAL devices button and table -->
+ <cd-osd-devices-selection-groups #walDeviceSelectionGroups
+ name="WAL"
+ type="wal"
+ [availDevices]="availDevices"
+ [canSelect]="dataDeviceSelectionGroups.devices.length !== 0"
+ (selected)="onDevicesSelected($event)"
+ (cleared)="onDevicesCleared($event)">
+ </cd-osd-devices-selection-groups>
- <!-- WAL slots -->
- <div class="form-group row"
- *ngIf="walDeviceSelectionGroups.devices.length !== 0">
- <label class="cd-col-form-label"
- for="walSlots">
- <ng-container i18n>WAL slots</ng-container>
- <cd-helper>
- <span i18n>How many OSDs per WAL device.</span>
- <br>
- <span i18n>Specify 0 to let Orchestrator backend decide it.</span>
- </cd-helper>
- </label>
- <div class="cd-col-form-input">
- <input class="form-control"
- id="walSlots"
- name="walSlots"
- type="number"
- min="0"
- formControlName="walSlots">
- <span class="invalid-feedback"
- *ngIf="form.showError('walSlots', formDir, 'min')"
- i18n>Value should be greater than or equal to 0</span>
- </div>
- </div>
+ <!-- WAL slots -->
+ <div class="form-group row"
+ *ngIf="walDeviceSelectionGroups.devices.length !== 0">
+ <label class="cd-col-form-label"
+ for="walSlots">
+ <ng-container i18n>WAL slots</ng-container>
+ <cd-helper>
+ <span i18n>How many OSDs per WAL device.</span>
+ <br>
+ <span i18n>Specify 0 to let Orchestrator backend decide it.</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ id="walSlots"
+ name="walSlots"
+ type="number"
+ min="0"
+ formControlName="walSlots">
+ <span class="invalid-feedback"
+ *ngIf="form.showError('walSlots', formDir, 'min')"
+ i18n>Value should be greater than or equal to 0</span>
+ </div>
+ </div>
- <!-- DB devices button and table -->
- <cd-osd-devices-selection-groups #dbDeviceSelectionGroups
- name="DB"
- type="db"
- [availDevices]="availDevices"
- [canSelect]="dataDeviceSelectionGroups.devices.length !== 0"
- (selected)="onDevicesSelected($event)"
- (cleared)="onDevicesCleared($event)">
- </cd-osd-devices-selection-groups>
+ <!-- DB devices button and table -->
+ <cd-osd-devices-selection-groups #dbDeviceSelectionGroups
+ name="DB"
+ type="db"
+ [availDevices]="availDevices"
+ [canSelect]="dataDeviceSelectionGroups.devices.length !== 0"
+ (selected)="onDevicesSelected($event)"
+ (cleared)="onDevicesCleared($event)">
+ </cd-osd-devices-selection-groups>
- <!-- DB slots -->
- <div class="form-group row"
- *ngIf="dbDeviceSelectionGroups.devices.length !== 0">
- <label class="cd-col-form-label"
- for="dbSlots">
- <ng-container i18n>DB slots</ng-container>
- <cd-helper>
- <span i18n>How many OSDs per DB device.</span>
- <br>
- <span i18n>Specify 0 to let Orchestrator backend decide it.</span>
- </cd-helper>
- </label>
- <div class="cd-col-form-input">
- <input class="form-control"
- id="dbSlots"
- name="dbSlots"
- type="number"
- min="0"
- formControlName="dbSlots">
- <span class="invalid-feedback"
- *ngIf="form.showError('dbSlots', formDir, 'min')"
- i18n>Value should be greater than or equal to 0</span>
+ <!-- DB slots -->
+ <div class="form-group row"
+ *ngIf="dbDeviceSelectionGroups.devices.length !== 0">
+ <label class="cd-col-form-label"
+ for="dbSlots">
+ <ng-container i18n>DB slots</ng-container>
+ <cd-helper>
+ <span i18n>How many OSDs per DB device.</span>
+ <br>
+ <span i18n>Specify 0 to let Orchestrator backend decide it.</span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ id="dbSlots"
+ name="dbSlots"
+ type="number"
+ min="0"
+ formControlName="dbSlots">
+ <span class="invalid-feedback"
+ *ngIf="form.showError('dbSlots', formDir, 'min')"
+ i18n>Value should be greater than or equal to 0</span>
+ </div>
+ </div>
+ </fieldset>
</div>
</div>
- </fieldset>
-
- <!-- Configuration -->
- <fieldset>
- <legend i18n>Configuration</legend>
+ </div>
- <!-- Features -->
- <div class="form-group row"
- formGroupName="features">
- <label i18n
- class="cd-col-form-label"
- for="features">Features</label>
- <div class="cd-col-form-input">
+ <!-- Features -->
+ <div class="card">
+ <div class="card-header">
+ <h2 class="mb-0">
+ <button class="btn btn-link btn-block text-left dropdown-toggle"
+ data-toggle="collapse"
+ aria-label="features"
+ aria-expanded="true"
+ i18n>Features</button>
+ </h2>
+ </div>
+ </div>
+ <div class="collapse show">
+ <div class="card-body d-flex flex-column">
+ <div class="pt-3 pb-3"
+ formGroupName="features">
<div class="custom-control custom-checkbox"
*ngFor="let feature of featureList">
<input type="checkbox"
class="custom-control-input"
id="{{ feature.key }}"
name="{{ feature.key }}"
- formControlName="{{ feature.key }}">
+ formControlName="{{ feature.key }}"
+ (change)="emitDeploymentSelection()">
<label class="custom-control-label"
for="{{ feature.key }}">{{ feature.desc }}</label>
</div>
</div>
</div>
- </fieldset>
- </div>
- <div class="card-footer"
- *ngIf="!hideSubmitBtn">
- <cd-form-button-panel #previewButtonPanel
- (submitActionEvent)="submit()"
- [form]="form"
- [disabled]="dataDeviceSelectionGroups.devices.length === 0"
- [submitText]="actionLabels.PREVIEW"
- wrappingClass="text-right"></cd-form-button-panel>
+ </div>
</div>
- </div>
- </form>
+ </form>
+ </div>
+
+ <div class="card-footer"
+ *ngIf="!hideSubmitBtn">
+ <cd-form-button-panel #previewButtonPanel
+ (submitActionEvent)="submit()"
+ [form]="form"
+ [disabled]="dataDeviceSelectionGroups.devices.length === 0 && !simpleDeployment"
+ [submitText]="simpleDeployment ? 'Create OSDs' : actionLabels.PREVIEW"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </div>
</div>
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';
}
];
+ 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);
SharedModule,
RouterTestingModule,
ReactiveFormsModule,
- ToastrModule.forRoot()
+ ToastrModule.forRoot(),
+ DashboardModule
],
declarations: [OsdFormComponent, OsdDevicesSelectionGroupsComponent, InventoryDevicesComponent]
});
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', () => {
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';
@Output() emitDriveGroup: EventEmitter<DriveGroup> = new EventEmitter();
+ @Output() emitDeploymentOption: EventEmitter<object> = new EventEmitter();
+
+ @Output() emitMode: EventEmitter<boolean> = new EventEmitter();
+
icons = Icons;
form: CdFormGroup;
hasOrchestrator = true;
+ simpleDeployment = true;
+
+ deploymentOptions: DeploymentOptions;
+ optionNames = Object.values(OsdDeploymentOptions);
+
constructor(
public actionLabels: ActionLabelsI18n,
private authStorageService: AuthStorageService,
private hostService: HostService,
private router: Router,
private modalService: ModalService,
- public wizardStepService: WizardStepsService
+ private osdService: OsdService,
+ private taskWrapper: TaskWrapperService
) {
super();
this.resource = $localize`OSDs`;
}
});
+ 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) => {
acc[e.key] = new FormControl({ value: false, disabled: true });
return acc;
}, {})
- )
+ ),
+ deploymentOption: new FormControl(0)
});
}
}
}
+ 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;
+ }
}
}
});
it('should call create', () => {
+ const trackingId = 'all_hdd, host1_ssd';
const post_data = {
method: 'drive_groups',
data: [
}
}
],
- 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);
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');
+ });
});
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';
})
export class OsdService {
private path = 'api/osd';
+ private uiPath = 'ui-api/osd';
+
osdDevices: InventoryDeviceType[] = [];
osdRecvSpeedModalPriorities = {
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' });
}
return this.http.post(`${this.path}/${id}/scrub?deep=${deep}`, null);
}
+ getDeploymentOptions() {
+ return this.http.get<DeploymentOptions>(`${this.uiPath}/deployment_options`);
+ }
+
getFlags() {
return this.http.get(`${this.path}/flags`);
}
@use './src/styles/vendor/variables' as vv;
+::ng-deep cd-wizard {
+ width: 15%;
+}
+
.card-body {
padding-left: 0;
}
--- /dev/null
+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;
+}
.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);
+ }
+ }
+ }
+}
--- /dev/null
+# -*- 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__
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
]
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'])