"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")
}
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)
[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",
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 {
path: `${URLVerbs.CREATE}`,
component: SmbClusterFormComponent,
data: { breadcrumbs: ActionLabels.CREATE }
+ },
+ {
+ path: `share/${URLVerbs.CREATE}/:clusterId`,
+ component: SmbShareFormComponent,
+ data: { breadcrumbs: ActionLabels.CREATE }
}
]
}
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';
join_sources: joinSourceObj
};
- const requestModel: RequestModel = {
+ const requestModel: ClusterRequestModel = {
cluster_resource: {
resource_type: CLUSTER_RESOURCE,
cluster_id: rawFormValue.cluster_id,
) {
super();
this.permission = this.authStorageService.getPermissions().smb;
- this.tableActions = [
- {
- permission: 'delete',
- icon: Icons.destroy,
- click: () => this.removeSMBClusterModal(),
- name: this.actionLabels.REMOVE
- }
- ];
}
ngOnInit() {
routerLink: () => this.urlBuilder.getCreate(),
canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection
+ },
+ {
+ permission: 'delete',
+ icon: Icons.destroy,
+ click: () => this.removeSMBClusterModal(),
+ name: this.actionLabels.REMOVE
}
];
--- /dev/null
+<div cdsCol
+ [columnNumbers]="{ md: 4 }">
+ <form name="smbShareForm"
+ #formDir="ngForm"
+ [formGroup]="smbShareForm"
+ novalidate>
+ <div i18n="form title"
+ class="form-header">
+ {{ action | titlecase }} {{ resource | upperFirst }}
+ </div>
+
+ <!-- Share Id -->
+ <div class="form-item">
+ <cds-text-label
+ labelInputID="share_id"
+ i18n
+ helperText="Unique share identifier"
+ i18n-helperText
+ cdRequiredField="Share Name"
+ [invalid]="smbShareForm.controls.share_id.invalid && smbShareForm.controls.share_id.dirty"
+ [invalidText]="shareError"
+ >Share Name
+ <input
+ cdsText
+ type="text"
+ id="share_id"
+ formControlName="share_id"
+ [invalid]="smbShareForm.controls.share_id.invalid && smbShareForm.controls.share_id.dirty"
+ />
+ </cds-text-label>
+ <ng-template #shareError>
+ <span
+ class="invalid-feedback"
+ *ngIf="smbShareForm.showError('share_id', formDir, 'required')"
+ i18n
+ >This field is required.</span
+ >
+ </ng-template>
+ </div>
+
+ <!-- Volume -->
+ <div class="form-item">
+ <cds-select
+ formControlName="volume"
+ label="Volume"
+ cdRequiredField="Volume"
+ id="volume"
+ (change)="volumeChangeHandler()"
+ [invalid]="smbShareForm.controls.volume.invalid && smbShareForm.controls.volume.dirty"
+ [invalidText]="volumeError"
+ i18n-label>
+ <option *ngIf="allFsNames?.length === 0"
+ value=""
+ i18n>
+ -- No filesystem available --
+ </option>
+ <option *ngIf="allFsNames !== null && allFsNames?.length > 0"
+ value=""
+ i18n>
+ -- Select the filesystem --
+ </option>
+ <option *ngFor="let filesystem of allFsNames"
+ [value]="filesystem.name"
+ i18n>
+ {{ filesystem.name }}
+ </option>
+ </cds-select>
+ <ng-template #volumeError>
+ <span
+ class="invalid-feedback"
+ *ngIf="smbShareForm.showError('volume', formDir, 'required')"
+ i18n
+ >This field is required.</span
+ >
+ </ng-template>
+ </div>
+
+ <div class="form-item"
+ *ngIf="smbShareForm.getValue('volume')">
+ <cds-select
+ formControlName="subvolume_group"
+ label="Subvolume Group"
+ id="subvolume_group"
+ (change)="getSubVol()"
+ [skeleton]="allsubvolgrps === null"
+ i18n-label>
+ <option *ngIf="allsubvolgrps === null"
+ value=""
+ i18n>Loading...</option>
+ <option *ngIf="allsubvolgrps !== null && allsubvolgrps.length >= 0"
+ value=""
+ i18n>
+ -- Select the CephFS subvolume group --
+ </option>
+ <option
+ *ngFor="let subvol_grp of allsubvolgrps"
+ [value]="subvol_grp.name"
+ [selected]="subvol_grp.name === smbShareForm.get('subvolume_group').value"
+ i18n
+ >
+ {{ subvol_grp.name }}
+ </option>
+ </cds-select>
+ </div>
+
+ <div class="form-group row"
+ *ngIf="smbShareForm.getValue('volume')">
+ <cds-select
+ formControlName="subvolume"
+ label="Subvolume"
+ id="subvolume"
+ (change)="setSubVolPath()"
+ [skeleton]="allsubvols === null"
+ >
+ <option *ngIf="allsubvols === null"
+ value=""
+ i18n>Loading...</option>
+ <option *ngIf="allsubvols !== null && allsubvols.length === 0"
+ value=""
+ i18n>
+ -- No SMB subvolume available --
+ </option>
+ <option *ngIf="allsubvols !== null && allsubvols.length > 0"
+ value=""
+ i18n>
+ -- Select the SMB subvolume --
+ </option>
+ <option
+ *ngFor="let subvolume of allsubvols"
+ [value]="subvolume.name"
+ [selected]="subvolume.name === smbShareForm.get('subvolume').value"
+ i18n
+ >
+ {{ subvolume.name }}
+ </option>
+ </cds-select>
+ </div>
+
+ <!-- Path -->
+ <div class="form-item form-item-append"
+ cdsRow>
+ <div cdsCol>
+ <cds-text-label labelInputID="prefixedPath"
+ i18n
+ helperText="A path is a relative path.">Prefixed Path
+ <input cdsText
+ type="text"
+ id="prefixedPath"
+ formControlName="prefixedPath" />
+ </cds-text-label>
+ </div>
+ <div cdsCol>
+ <cds-text-label
+ labelInputID="inputPath"
+ i18n
+ [invalid]="
+ smbShareForm.controls.inputPath.invalid && smbShareForm.controls.inputPath.dirty
+ "
+ [invalidText]="pathError"
+ helperText="A relative path in a cephFS file system."
+ cdRequiredField="Path"
+ >Input Path
+ <input
+ cdsText
+ type="text"
+ id="inputPath"
+ formControlName="inputPath"
+ [invalid]="
+ smbShareForm.controls.inputPath.invalid && smbShareForm.controls.inputPath.dirty
+ "
+ />
+ </cds-text-label>
+ <ng-template #pathError>
+ <span
+ class="invalid-feedback"
+ *ngIf="smbShareForm.showError('inputPath', formDir, 'required')"
+ i18n
+ >This field is required.</span
+ >
+ <span
+ class="invalid-feedback"
+ *ngIf="smbShareForm.showError('inputPath', formDir, 'pattern')"
+ i18n
+ >Path need to start with a '/' and can be followed by a word</span
+ >
+ </ng-template>
+ </div>
+ </div>
+
+ <!-- Browseable -->
+ <div class="form-item">
+ <cds-checkbox id="browseable"
+ formControlName="browseable"
+ i18n>Browseable
+ <cd-help-text
+ >If selected the share will be included in share listings visible to
+ clients.</cd-help-text
+ >
+ </cds-checkbox>
+ </div>
+
+ <!-- Readonly -->
+ <div class="form-item">
+ <cds-checkbox id="readonly"
+ formControlName="readonly"
+ i18n>Readonly
+ <cd-help-text>If selected no clients are permitted to write to the share.</cd-help-text>
+ </cds-checkbox>
+ </div>
+ <cd-form-button-panel
+ (submitActionEvent)="submitAction()"
+ [form]="smbShareForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+ wrappingClass="text-right"
+ ></cd-form-button-panel>
+ </form>
+</div>
--- /dev/null
+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<SmbShareFormComponent>;
+
+ 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();
+ });
+});
--- /dev/null
+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<void> {
+ return new Promise<void>((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 });
+ }
+ });
+ }
+}
[hasDetails]="false"
(fetchData)="loadSMBShares()"
>
+ <div class="table-actions">
+ <cd-table-actions
+ class="btn-group"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions"
+ >
+ </cd-table-actions>
+ </div>
</cd-table>
- </ng-container>
+</ng-container>
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',
table: TableComponent;
columns: CdTableColumn[];
permission: Permission;
+ selection = new CdTableSelection();
+ tableActions: CdTableAction[];
context: CdTableFetchDataContext;
smbShares$: Observable<SMBShare[]>;
subject$ = new BehaviorSubject<SMBShare[]>([]);
- constructor(private authStorageService: AuthStorageService, private smbService: SmbService) {
+ constructor(
+ private authStorageService: AuthStorageService,
+ public actionLabels: ActionLabelsI18n,
+ private smbService: SmbService
+ ) {
this.permission = this.authStorageService.getPermissions().smb;
}
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(() =>
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',
};
export interface SMBShare {
+ resource_type: string;
cluster_id: string;
share_id: string;
- intent: string;
cephfs: SMBCephfs;
+ intent?: string;
name?: string;
readonly?: boolean;
browseable?: boolean;
type Intent = 'present' | 'removed';
export const CLUSTER_RESOURCE = 'ceph.smb.cluster';
+
+export const SHARE_RESOURCE = 'ceph.smb.share';
+
+export const PROVIDER = 'samba-vfs';
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: [
SmbUsersgroupsListComponent,
SmbUsersgroupsDetailsComponent,
SmbTabsComponent,
- SmbJoinAuthListComponent
- ]
+ SmbJoinAuthListComponent,
+ SmbShareFormComponent
+ ],
+ providers: [provideCharts(withDefaultRegisterables())]
})
export class SmbModule {
constructor(private iconService: IconService) {
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');
});
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');
+ });
});
import { Observable, Subject } from 'rxjs';
import {
+ ClusterRequestModel,
DomainSettings,
+ ShareRequestModel,
SMBCluster,
SMBJoinAuth,
SMBShare,
return this.http.get<SMBCluster[]>(`${this.baseURL}/cluster`);
}
- createCluster(requestModel: any) {
+ createCluster(requestModel: ClusterRequestModel) {
return this.http.post(`${this.baseURL}/cluster`, requestModel);
}
listUsersGroups(): Observable<SMBUsersGroups[]> {
return this.http.get<SMBUsersGroups[]>(`${this.baseURL}/usersgroups`);
}
+
+ createShare(requestModel: ShareRequestModel) {
+ return this.http.post(`${this.baseURL}/share`, requestModel);
+ }
}
// 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)
)
};
}'`;
}
- 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}'`;
}
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
- volume
- path
- provider
+ - subvolumegroup
+ - subvolume
type: object
cluster_id:
description: Unique identifier for the cluster
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 \