]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: SMB Create Share 61588/head
authorDnyaneshwari <dnyaneshwari@li-9c9fbecc-2d5c-11b2-a85c-e2a7cc8a424f.ibm.com>
Thu, 30 Jan 2025 08:14:06 +0000 (13:44 +0530)
committerDnyaneshwari <dnyaneshwari@li-9c9fbecc-2d5c-11b2-a85c-e2a7cc8a424f.ibm.com>
Fri, 14 Feb 2025 11:25:51 +0000 (16:55 +0530)
Fixes: https://tracker.ceph.com/issues/69733
Signed-off-by: Dnyaneshwari Talwekar <dtalweka@redhat.com>
16 files changed:
src/pybind/mgr/dashboard/controllers/smb.py
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts
src/pybind/mgr/dashboard/openapi.yaml

index ae06b82ad608b0dbdae96c4f1b6b7716bcb0af1f..7a1390f151b7c59ad91da13d62aaaa9f8d3d57d1 100644 (file)
@@ -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",
index 49d02b430dd5fd28faa8a30b8560fa948a673a58..7fb568cad79bee3f50b19f0200018cc3e762e312 100644 (file)
@@ -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 }
               }
             ]
           }
index 5bebe3cc435d0177ac83d36bdf853e18ab58ddbd..2ab36f571316aec428a74ed7f9a7e60d4c1b7f88 100644 (file)
@@ -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,
index f6cd1cdbd061312d3942e95e4fb712656e0233a3..d1217d87fb1978915a3340f34eea5651aff32409 100644 (file)
@@ -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 (file)
index 0000000..3691116
--- /dev/null
@@ -0,0 +1,217 @@
+<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>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..1d4f3c1
--- /dev/null
@@ -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<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();
+  });
+});
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 (file)
index 0000000..fce6e75
--- /dev/null
@@ -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<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 });
+        }
+      });
+  }
+}
index 54cc55a2b8d8cb6dfebb21ac750a8e67d6319dd3..010b510f3ee4e9460e0ba49d4f5ce6abed6384c4 100644 (file)
@@ -7,5 +7,14 @@
     [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>
index 466d8dc4318b2cf834d5c3f9e08e21ce0d92116b..d352533a423a8b63ae179842a42fcb1a6f590e49 100644 (file)
@@ -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<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;
   }
 
@@ -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(() =>
index 3c1286da775f03bd1a8a32e89ce9b1bf8307783f..1d4aff01353a7850cf1c5aecf68938150a9f0ee0 100644 (file)
@@ -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';
index 2211e8629bb4557558b7533785d3711168ff2423..a375da67e743ba9d20372cc61690975b698a6170 100644 (file)
@@ -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) {
index 8beee92bd4327f8d1e636e93be90f4c01f2d0f6f..014d614dda8bfa306991ac7b0384b1b71a21ebda 100644 (file)
@@ -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');
+  });
 });
index ac2e460b08139434a3ac781f2a1f26637d840c2f..ae6ef3303237c86755b763d99d133d667f41d664 100644 (file)
@@ -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<SMBCluster[]>(`${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<SMBUsersGroups[]> {
     return this.http.get<SMBUsersGroups[]>(`${this.baseURL}/usersgroups`);
   }
+
+  createShare(requestModel: ShareRequestModel) {
+    return this.http.post(`${this.baseURL}/share`, requestModel);
+  }
 }
index 60c62ec09d07b9e40e4dbb7cb805a8cef3dc8804..708cc72fe295fcd3098b2e2276b140907ffd87d8 100644 (file)
@@ -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}'`;
   }
index 3dc4ef2b9632c637408ddb443e8397c336fc226e..3cd37e26d3a3da924cdbe08be991bbaf939cdbf6 100644 (file)
@@ -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       \