From: Dnyaneshwari Date: Thu, 30 Jan 2025 08:14:06 +0000 (+0530) Subject: mgr/dashboard: SMB Create Share X-Git-Tag: v20.0.0~148^2 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=56c8c5199726b2da6c4744d770267219cda9be7b;p=ceph.git mgr/dashboard: SMB Create Share Fixes: https://tracker.ceph.com/issues/69733 Signed-off-by: Dnyaneshwari Talwekar --- diff --git a/src/pybind/mgr/dashboard/controllers/smb.py b/src/pybind/mgr/dashboard/controllers/smb.py index ae06b82ad608b..7a1390f151b7c 100644 --- a/src/pybind/mgr/dashboard/controllers/smb.py +++ b/src/pybind/mgr/dashboard/controllers/smb.py @@ -84,7 +84,9 @@ SHARE_SCHEMA = { "cephfs": ({ "volume": (str, "Name of the CephFS file system"), "path": (str, "Path within the CephFS file system"), - "provider": (str, "Provider of the CephFS share, e.g., 'samba-vfs'") + "provider": (str, "Provider of the CephFS share, e.g., 'samba-vfs'"), + "subvolumegroup": (str, "Subvolume Group in CephFS file system"), + "subvolume": (str, "Subvolume within the CephFS file system"), }, "Configuration for the CephFS share") } @@ -123,6 +125,30 @@ USERSGROUPS_SCHEMA = { LIST_USERSGROUPS_SCHEMA = [USERSGROUPS_SCHEMA] +SHARE_SCHEMA_RESULTS = { + "results": ([{ + "resource": ({ + "resource_type": (str, "ceph.smb.share"), + "cluster_id": (str, "Unique identifier for the cluster"), + "share_id": (str, "Unique identifier for the share"), + "intent": (str, "Desired state of the resource, e.g., 'present' or 'removed'"), + "name": (str, "Name of the share"), + "readonly": (bool, "Indicates if the share is read-only"), + "browseable": (bool, "Indicates if the share is browseable"), + "cephfs": ({ + "volume": (str, "Name of the CephFS file system"), + "path": (str, "Path within the CephFS file system"), + "subvolumegroup": (str, "Subvolume Group in CephFS file system"), + "subvolume": (str, "Subvolume within the CephFS file system"), + "provider": (str, "Provider of the CephFS share, e.g., 'samba-vfs'") + }, "Configuration for the CephFS share") + }, "Resource details"), + "state": (str, "State of the resource"), + "success": (bool, "Indicates whether the operation was successful") + }], "List of results with resource details"), + "success": (bool, "Overall success status of the operation") +} + def raise_on_failure(func): @wraps(func) @@ -238,6 +264,29 @@ class SMBShare(RESTController): [f'{self._resource}.{cluster_id}' if cluster_id else self._resource]) return res['resources'] if 'resources' in res else [res] + @raise_on_failure + @CreatePermission + @EndpointDoc("Create smb share", + parameters={ + 'share_resource': (str, 'share_resource') + }, + responses={201: SHARE_SCHEMA_RESULTS}) + def create(self, share_resource: Share) -> Simplified: + """ + Create an smb share + + :param share_resource: Dict share data + :return: Returns share resource. + :rtype: Dict[str, Any] + """ + try: + return mgr.remote( + 'smb', + 'apply_resources', + json.dumps(share_resource)).to_simplified() + except RuntimeError as e: + raise DashboardException(e, component='smb') + @raise_on_failure @DeletePermission @EndpointDoc("Remove an smb share", diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 49d02b430dd5f..7fb568cad79be 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -53,6 +53,7 @@ import { MultiClusterListComponent } from './ceph/cluster/multi-cluster/multi-cl import { MultiClusterDetailsComponent } from './ceph/cluster/multi-cluster/multi-cluster-details/multi-cluster-details.component'; import { SmbClusterFormComponent } from './ceph/smb/smb-cluster-form/smb-cluster-form.component'; import { SmbTabsComponent } from './ceph/smb/smb-tabs/smb-tabs.component'; +import { SmbShareFormComponent } from './ceph/smb/smb-share-form/smb-share-form.component'; @Injectable() export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver { @@ -451,6 +452,11 @@ const routes: Routes = [ path: `${URLVerbs.CREATE}`, component: SmbClusterFormComponent, data: { breadcrumbs: ActionLabels.CREATE } + }, + { + path: `share/${URLVerbs.CREATE}/:clusterId`, + component: SmbShareFormComponent, + data: { breadcrumbs: ActionLabels.CREATE } } ] } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts index 5bebe3cc435d0..2ab36f571316a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts @@ -8,11 +8,11 @@ import { AUTHMODE, CLUSTERING, PLACEMENT, - RequestModel, - CLUSTER_RESOURCE, RESOURCE, DomainSettings, - JoinSource + JoinSource, + CLUSTER_RESOURCE, + ClusterRequestModel } from '../smb.model'; import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; import { Icons } from '~/app/shared/enum/icons.enum'; @@ -212,7 +212,7 @@ export class SmbClusterFormComponent extends CdForm implements OnInit { join_sources: joinSourceObj }; - const requestModel: RequestModel = { + const requestModel: ClusterRequestModel = { cluster_resource: { resource_type: CLUSTER_RESOURCE, cluster_id: rawFormValue.cluster_id, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.ts index f6cd1cdbd0613..d1217d87fb197 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.ts @@ -54,14 +54,6 @@ export class SmbClusterListComponent extends ListWithDetails implements OnInit { ) { super(); this.permission = this.authStorageService.getPermissions().smb; - this.tableActions = [ - { - permission: 'delete', - icon: Icons.destroy, - click: () => this.removeSMBClusterModal(), - name: this.actionLabels.REMOVE - } - ]; } ngOnInit() { @@ -85,6 +77,12 @@ export class SmbClusterListComponent extends ListWithDetails implements OnInit { routerLink: () => this.urlBuilder.getCreate(), canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection + }, + { + permission: 'delete', + icon: Icons.destroy, + click: () => this.removeSMBClusterModal(), + name: this.actionLabels.REMOVE } ]; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.html new file mode 100644 index 0000000000000..3691116984d00 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.html @@ -0,0 +1,217 @@ +
+
+
+ {{ action | titlecase }} {{ resource | upperFirst }} +
+ + +
+ Share Name + + + + This field is required. + +
+ + +
+ + + + + + + This field is required. + +
+ +
+ + + + + +
+ +
+ + + + + + +
+ + +
+
+ Prefixed Path + + +
+
+ Input Path + + + + This field is required. + Path need to start with a '/' and can be followed by a word + +
+
+ + +
+ Browseable + If selected the share will be included in share listings visible to + clients. + +
+ + +
+ Readonly + If selected no clients are permitted to write to the share. + +
+ +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.spec.ts new file mode 100644 index 0000000000000..1d4f3c104c5c8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.spec.ts @@ -0,0 +1,96 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SmbShareFormComponent } from './smb-share-form.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { SharedModule } from '~/app/shared/shared.module'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ReactiveFormsModule, Validators } from '@angular/forms'; +import { ToastrModule } from 'ngx-toastr'; +import { + CheckboxModule, + ComboBoxModule, + GridModule, + InputModule, + SelectModule +} from 'carbon-components-angular'; +import { SmbService } from '~/app/shared/api/smb.service'; +import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; + +describe('SmbShareFormComponent', () => { + let component: SmbShareFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + BrowserAnimationsModule, + SharedModule, + HttpClientTestingModule, + RouterTestingModule, + ReactiveFormsModule, + ToastrModule.forRoot(), + GridModule, + InputModule, + SelectModule, + ComboBoxModule, + CheckboxModule + ], + declarations: [SmbShareFormComponent], + providers: [SmbService, TaskWrapperService] + }).compileComponents(); + + fixture = TestBed.createComponent(SmbShareFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should create the form', () => { + component.ngOnInit(); + expect(component.smbShareForm).toBeDefined(); + expect(component.smbShareForm.get('share_id')).toBeTruthy(); + expect(component.smbShareForm.get('volume')).toBeTruthy(); + expect(component.smbShareForm.get('subvolume_group')).toBeTruthy(); + expect(component.smbShareForm.get('prefixedPath')).toBeTruthy(); + }); + + it('should update subvolume group when volume changes', () => { + component.smbShareForm.get('volume').setValue('fs1'); + component.smbShareForm.get('subvolume').setValue('subvol1'); + component.volumeChangeHandler(); + expect(component.smbShareForm.get('subvolume_group').value).toBe(''); + expect(component.smbShareForm.get('subvolume').value).toBe(''); + }); + + it('should call getSubVolGrp when volume is selected', () => { + const fsName = 'fs1'; + component.smbShareForm.get('volume').setValue(fsName); + component.volumeChangeHandler(); + expect(component).toBeTruthy(); + }); + + it('should set the correct subvolume validation', () => { + component.smbShareForm.get('subvolume_group').setValue(''); + expect(component.smbShareForm.get('subvolume').hasValidator(Validators.required)).toBe(false); + component.smbShareForm.get('subvolume_group').setValue('otherGroup'); + expect(component.smbShareForm.get('subvolume').hasValidator(Validators.required)).toBe(false); + }); + + it('should call submitAction', () => { + component.smbShareForm.setValue({ + share_id: 'share1', + volume: 'fs1', + subvolume_group: 'group1', + subvolume: 'subvol1', + prefixedPath: '/volumes/fs1/group1/subvol1', + inputPath: '/', + browseable: true, + readonly: false + }); + component.submitAction(); + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.ts new file mode 100644 index 0000000000000..fce6e75b225e8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.ts @@ -0,0 +1,190 @@ +import { Component, OnInit } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { CdForm } from '~/app/shared/forms/cd-form'; +import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder'; +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; + +import _ from 'lodash'; +import { map } from 'rxjs/operators'; + +import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; +import { FinishedTask } from '~/app/shared/models/finished-task'; + +import { Filesystem, PROVIDER, SHARE_RESOURCE, ShareRequestModel } from '../smb.model'; +import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolume-group.model'; +import { CephfsSubvolume } from '~/app/shared/models/cephfs-subvolume.model'; + +import { SmbService } from '~/app/shared/api/smb.service'; +import { NfsService } from '~/app/shared/api/nfs.service'; +import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; +import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-group.service'; +import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service'; + +@Component({ + selector: 'cd-smb-share-form', + templateUrl: './smb-share-form.component.html', + styleUrls: ['./smb-share-form.component.scss'] +}) +export class SmbShareFormComponent extends CdForm implements OnInit { + smbShareForm: CdFormGroup; + action: string; + resource: string; + allFsNames: Filesystem[] = []; + allsubvolgrps: CephfsSubvolumeGroup[] = []; + allsubvols: CephfsSubvolume[] = []; + clusterId: string; + + constructor( + private formBuilder: CdFormBuilder, + public smbService: SmbService, + public actionLabels: ActionLabelsI18n, + private nfsService: NfsService, + private subvolgrpService: CephfsSubvolumeGroupService, + private subvolService: CephfsSubvolumeService, + private taskWrapperService: TaskWrapperService, + private router: Router, + private route: ActivatedRoute + ) { + super(); + this.resource = $localize`Share`; + } + ngOnInit() { + this.action = this.actionLabels.CREATE; + this.route.params.subscribe((params: { clusterId: string }) => { + this.clusterId = params.clusterId; + }); + this.nfsService.filesystems().subscribe((data: Filesystem[]) => { + this.allFsNames = data; + }); + this.createForm(); + } + + createForm() { + this.smbShareForm = this.formBuilder.group({ + share_id: new FormControl('', { + validators: [Validators.required] + }), + volume: new FormControl('', { + validators: [Validators.required] + }), + subvolume_group: new FormControl(''), + subvolume: new FormControl(''), + prefixedPath: new FormControl({ value: '', disabled: true }), + inputPath: new FormControl('/', { + validators: [Validators.required] + }), + browseable: new FormControl(true), + readonly: new FormControl(false) + }); + } + + volumeChangeHandler() { + const fsName = this.smbShareForm.getValue('volume'); + this.smbShareForm.patchValue({ + subvolume_group: '', + subvolume: '', + prefixedPath: '' + }); + this.allsubvols = []; + if (fsName) { + this.getSubVolGrp(fsName); + } + } + + getSubVolGrp(volume: string) { + this.smbShareForm.patchValue({ + subvolume_group: '', + subvolume: '' + }); + if (volume) { + this.subvolgrpService.get(volume).subscribe((data: CephfsSubvolumeGroup[]) => { + this.allsubvolgrps = data; + }); + } + } + + async getSubVol() { + const volume = this.smbShareForm.getValue('volume'); + const subvolgrp = this.smbShareForm.getValue('subvolume_group'); + this.smbShareForm.patchValue({ + subvolume: '', + prefixedPath: '' + }); + this.allsubvols = []; + + if (volume && subvolgrp) { + await this.setSubVolPath(); + this.subvolService.get(volume, subvolgrp, false).subscribe((data: CephfsSubvolume[]) => { + this.allsubvols = data; + }); + } + } + + setSubVolPath(): Promise { + return new Promise((resolve, reject) => { + const fsName = this.smbShareForm.getValue('volume'); + const subvolGroup = this.smbShareForm.getValue('subvolume_group') || ''; // Default to empty if not present + const subvol = this.smbShareForm.getValue('subvolume'); + + this.subvolService + .info(fsName, subvol, subvolGroup) + .pipe(map((data: any) => data['path'])) + .subscribe( + (path: string) => { + this.updatePath(path); + resolve(); + }, + (error: any) => reject(error) + ); + }); + } + + updatePath(prefixedPath: string) { + this.smbShareForm.patchValue({ prefixedPath: prefixedPath }); + } + + buildRequest() { + const rawFormValue = _.cloneDeep(this.smbShareForm.value); + const correctedPath = rawFormValue.inputPath; + const requestModel: ShareRequestModel = { + share_resource: { + resource_type: SHARE_RESOURCE, + cluster_id: this.clusterId, + share_id: rawFormValue.share_id, + cephfs: { + volume: rawFormValue.volume, + path: correctedPath, + subvolumegroup: rawFormValue.subvolume_group, + subvolume: rawFormValue.subvolume, + provider: PROVIDER + }, + browseable: rawFormValue.browseable, + readonly: rawFormValue.readonly + } + }; + + return requestModel; + } + + submitAction() { + const component = this; + const requestModel = this.buildRequest(); + const BASE_URL = 'smb/share'; + const share_id = this.smbShareForm.get('share_id').value; + const taskUrl = `${BASE_URL}/${URLVerbs.CREATE}`; + this.taskWrapperService + .wrapTaskAroundCall({ + task: new FinishedTask(taskUrl, { share_id }), + call: this.smbService.createShare(requestModel) + }) + .subscribe({ + complete: () => { + this.router.navigate([`cephfs/smb`]); + }, + error() { + component.smbShareForm.setErrors({ cdSubmitButton: true }); + } + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.html index 54cc55a2b8d8c..010b510f3ee4e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.html @@ -7,5 +7,14 @@ [hasDetails]="false" (fetchData)="loadSMBShares()" > +
+ + +
- + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.ts index 466d8dc4318b2..d352533a423a8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.ts @@ -1,14 +1,18 @@ import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { Observable, BehaviorSubject, of } from 'rxjs'; +import { switchMap, catchError } from 'rxjs/operators'; import { TableComponent } from '~/app/shared/datatable/table/table.component'; +import { CdTableAction } from '~/app/shared/models/cd-table-action'; import { CdTableColumn } from '~/app/shared/models/cd-table-column'; import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context'; import { Permission } from '~/app/shared/models/permissions'; import { SMBShare } from '../smb.model'; -import { switchMap, catchError } from 'rxjs/operators'; import { SmbService } from '~/app/shared/api/smb.service'; import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; +import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { Icons } from '~/app/shared/enum/icons.enum'; @Component({ selector: 'cd-smb-share-list', @@ -22,12 +26,18 @@ export class SmbShareListComponent implements OnInit { table: TableComponent; columns: CdTableColumn[]; permission: Permission; + selection = new CdTableSelection(); + tableActions: CdTableAction[]; context: CdTableFetchDataContext; smbShares$: Observable; subject$ = new BehaviorSubject([]); - constructor(private authStorageService: AuthStorageService, private smbService: SmbService) { + constructor( + private authStorageService: AuthStorageService, + public actionLabels: ActionLabelsI18n, + private smbService: SmbService + ) { this.permission = this.authStorageService.getPermissions().smb; } @@ -70,6 +80,15 @@ export class SmbShareListComponent implements OnInit { flexGrow: 2 } ]; + this.tableActions = [ + { + name: `${this.actionLabels.CREATE}`, + permission: 'create', + icon: Icons.add, + routerLink: () => ['/cephfs/smb/share/create', this.clusterId], + canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection + } + ]; this.smbShares$ = this.subject$.pipe( switchMap(() => diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts index 3c1286da775f0..1d4aff01353a7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts @@ -12,24 +12,45 @@ export interface SMBCluster { public_addrs?: PublicAddress; } -export interface RequestModel { +export interface ClusterRequestModel { cluster_resource: SMBCluster; } -export interface DomainSettings { - realm?: string; - join_sources?: JoinSource[]; +export interface ShareRequestModel { + share_resource: SMBShare; } -export interface JoinSource { - source_type: string; - ref: string; +interface SMBCephfs { + volume: string; + path: string; + subvolumegroup?: string; + subvolume?: string; + provider?: string; +} + +interface SMBShareLoginControl { + name: string; + access: 'read' | 'read-write' | 'none' | 'admin'; + category?: 'user' | 'group'; +} + +export interface Filesystem { + id: string; + name: string; } +export interface DomainSettings { + realm?: string; + join_sources?: JoinSource[]; +} export interface PublicAddress { address: string; destination: string; } +export interface JoinSource { + source_type: string; + ref: string; +} export const CLUSTERING = { Default: 'default', @@ -53,10 +74,11 @@ export const PLACEMENT = { }; export interface SMBShare { + resource_type: string; cluster_id: string; share_id: string; - intent: string; cephfs: SMBCephfs; + intent?: string; name?: string; readonly?: boolean; browseable?: boolean; @@ -116,3 +138,7 @@ interface Value { type Intent = 'present' | 'removed'; export const CLUSTER_RESOURCE = 'ceph.smb.cluster'; + +export const SHARE_RESOURCE = 'ceph.smb.share'; + +export const PROVIDER = 'samba-vfs'; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts index 2211e8629bb45..a375da67e743b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts @@ -28,11 +28,13 @@ import { CommonModule } from '@angular/common'; import { SharedModule } from '~/app/shared/shared.module'; import { RouterModule } from '@angular/router'; import { NgModule } from '@angular/core'; +import { SmbShareFormComponent } from './smb-share-form/smb-share-form.component'; import { SmbUsersgroupsListComponent } from './smb-usersgroups-list/smb-usersgroups-list.component'; import { SmbTabsComponent } from './smb-tabs/smb-tabs.component'; import { SmbJoinAuthListComponent } from './smb-join-auth-list/smb-join-auth-list.component'; import { SmbUsersgroupsDetailsComponent } from './smb-usersgroups-details/smb-usersgroups-details.component'; +import { provideCharts, withDefaultRegisterables } from 'ng2-charts'; @NgModule({ imports: [ @@ -71,8 +73,10 @@ import { SmbUsersgroupsDetailsComponent } from './smb-usersgroups-details/smb-us SmbUsersgroupsListComponent, SmbUsersgroupsDetailsComponent, SmbTabsComponent, - SmbJoinAuthListComponent - ] + SmbJoinAuthListComponent, + SmbShareFormComponent + ], + providers: [provideCharts(withDefaultRegisterables())] }) export class SmbModule { constructor(private iconService: IconService) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts index 8beee92bd4327..014d614dda8bf 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts @@ -29,8 +29,29 @@ describe('SmbService', () => { expect(req.request.method).toBe('GET'); }); - it('should call create', () => { - service.createCluster('test').subscribe(); + it('should call create cluster', () => { + const request = { + cluster_resource: { + resource_type: 'ceph.smb.cluster', + cluster_id: 'clusterUserTest', + auth_mode: 'active-directory', + intent: 'present', + domain_settings: { + realm: 'DOMAIN1.SINK.TEST', + join_sources: [ + { + source_type: 'resource', + ref: 'join1-admin' + } + ] + }, + custom_dns: ['192.168.76.204'], + placement: { + count: 1 + } + } + }; + service.createCluster(request).subscribe(); const req = httpTesting.expectOne('api/smb/cluster'); expect(req.request.method).toBe('POST'); }); @@ -58,4 +79,26 @@ describe('SmbService', () => { const req = httpTesting.expectOne('api/smb/usersgroups'); expect(req.request.method).toBe('GET'); }); + + it('should call create share', () => { + const request = { + share_resource: { + resource_type: 'ceph.smb.share', + cluster_id: 'clusterUserTest', + share_id: 'share1', + intent: 'present', + name: 'share1name', + readonly: false, + browseable: true, + cephfs: { + volume: 'fs1', + path: '/', + provider: 'samba-vfs' + } + } + }; + service.createShare(request).subscribe(); + const req = httpTesting.expectOne('api/smb/share'); + expect(req.request.method).toBe('POST'); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts index ac2e460b08139..ae6ef3303237c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts @@ -3,7 +3,9 @@ import { Injectable } from '@angular/core'; import { Observable, Subject } from 'rxjs'; import { + ClusterRequestModel, DomainSettings, + ShareRequestModel, SMBCluster, SMBJoinAuth, SMBShare, @@ -28,7 +30,7 @@ export class SmbService { return this.http.get(`${this.baseURL}/cluster`); } - createCluster(requestModel: any) { + createCluster(requestModel: ClusterRequestModel) { return this.http.post(`${this.baseURL}/cluster`, requestModel); } @@ -49,4 +51,8 @@ export class SmbService { listUsersGroups(): Observable { return this.http.get(`${this.baseURL}/usersgroups`); } + + createShare(requestModel: ShareRequestModel) { + return this.http.post(`${this.baseURL}/share`, requestModel); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts index 60c62ec09d07b..708cc72fe295f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts @@ -490,6 +490,9 @@ export class TaskMessageService { // smb 'smb/cluster/create': this.newTaskMessage(this.commonOperations.create, (metadata) => this.smbCluster(metadata) + ), + 'smb/share/create': this.newTaskMessage(this.commonOperations.create, (metadata) => + this.smbShare(metadata) ) }; @@ -550,10 +553,14 @@ export class TaskMessageService { }'`; } - smbCluster(metadata: any) { + smbCluster(metadata: { cluster_id: string }) { return $localize`SMB Cluster '${metadata.cluster_id}'`; } + smbShare(metadata: { share_id: string }) { + return $localize`SMB Share '${metadata.share_id}'`; + } + service(metadata: any) { return $localize`service '${metadata.service_name}'`; } diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 3dc4ef2b9632c..3cd37e26d3a3d 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -15110,6 +15110,12 @@ paths: provider: description: Provider of the CephFS share, e.g., 'samba-vfs' type: string + subvolume: + description: Subvolume within the CephFS file system + type: string + subvolumegroup: + description: Subvolume Group in CephFS file system + type: string volume: description: Name of the CephFS file system type: string @@ -15117,6 +15123,8 @@ paths: - volume - path - provider + - subvolumegroup + - subvolume type: object cluster_id: description: Unique identifier for the cluster @@ -15162,6 +15170,132 @@ paths: summary: List smb shares tags: - SMB + post: + description: "\n Create an smb share\n\n :param share_resource:\ + \ Dict share data\n :return: Returns share resource.\n :rtype:\ + \ Dict[str, Any]\n " + parameters: [] + requestBody: + content: + application/json: + schema: + properties: + share_resource: + description: share_resource + type: string + required: + - share_resource + type: object + responses: + '201': + content: + application/vnd.ceph.api.v1.0+json: + schema: + properties: + results: + description: List of results with resource details + items: + properties: + resource: + description: Resource details + properties: + browseable: + description: Indicates if the share is browseable + type: boolean + cephfs: + description: Configuration for the CephFS share + properties: + path: + description: Path within the CephFS file system + type: string + provider: + description: Provider of the CephFS share, e.g., + 'samba-vfs' + type: string + subvolume: + description: Subvolume within the CephFS file system + type: string + subvolumegroup: + description: Subvolume Group in CephFS file system + type: string + volume: + description: Name of the CephFS file system + type: string + required: + - volume + - path + - subvolumegroup + - subvolume + - provider + type: object + cluster_id: + description: Unique identifier for the cluster + type: string + intent: + description: Desired state of the resource, e.g., 'present' + or 'removed' + type: string + name: + description: Name of the share + type: string + readonly: + description: Indicates if the share is read-only + type: boolean + resource_type: + description: ceph.smb.share + type: string + share_id: + description: Unique identifier for the share + type: string + required: + - resource_type + - cluster_id + - share_id + - intent + - name + - readonly + - browseable + - cephfs + type: object + state: + description: State of the resource + type: string + success: + description: Indicates whether the operation was successful + type: boolean + required: + - resource + - state + - success + type: object + type: array + success: + description: Overall success status of the operation + type: boolean + required: + - results + - success + type: object + description: Resource created. + '202': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Operation is still executing. Please check the task queue. + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + summary: Create smb share + tags: + - SMB /api/smb/share/{cluster_id}/{share_id}: delete: description: "\n Remove an smb share from a given cluster\n\n \