]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: cephfs subvolume creation form 52786/head
authorNizamudeen A <nia@redhat.com>
Thu, 3 Aug 2023 12:00:40 +0000 (17:30 +0530)
committerNizamudeen A <nia@redhat.com>
Mon, 7 Aug 2023 13:02:15 +0000 (18:32 +0530)
Fixes: https://tracker.ceph.com/issues/62345
Signed-off-by: Nizamudeen A <nia@redhat.com>
16 files changed:
src/pybind/mgr/dashboard/controllers/cephfs.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts
src/pybind/mgr/dashboard/openapi.yaml

index c38cc940fd8b794fd68f6a0018533363c4a23f7f..69de90f3780ba86ab4064e650a632acd1a1580bc 100644 (file)
@@ -641,7 +641,7 @@ class CephFSSubvolume(RESTController):
         error_code, out, err = mgr.remote(
             'volumes', '_cmd_fs_subvolume_ls', None, {'vol_name': vol_name})
         if error_code != 0:
-            raise RuntimeError(
+            raise DashboardException(
                 f'Failed to list subvolumes for volume {vol_name}: {err}'
             )
         subvolumes = json.loads(out)
@@ -649,8 +649,28 @@ class CephFSSubvolume(RESTController):
             error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None, {
                                               'vol_name': vol_name, 'sub_name': subvolume['name']})
             if error_code != 0:
-                raise RuntimeError(
+                raise DashboardException(
                     f'Failed to get info for subvolume {subvolume["name"]}: {err}'
                 )
             subvolume['info'] = json.loads(out)
         return subvolumes
+
+    @RESTController.Resource('GET')
+    def info(self, vol_name: str, subvol_name: str):
+        error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None, {
+            'vol_name': vol_name, 'sub_name': subvol_name})
+        if error_code != 0:
+            raise DashboardException(
+                f'Failed to get info for subvolume {subvol_name}: {err}'
+            )
+        return json.loads(out)
+
+    def create(self, vol_name: str, subvol_name: str, **kwargs):
+        error_code, _, err = mgr.remote('volumes', '_cmd_fs_subvolume_create', None, {
+            'vol_name': vol_name, 'sub_name': subvol_name, **kwargs})
+        if error_code != 0:
+            raise DashboardException(
+                f'Failed to create subvolume {subvol_name}: {err}'
+            )
+
+        return f'Subvolume {subvol_name} created successfully'
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.html
new file mode 100644 (file)
index 0000000..4e3bb68
--- /dev/null
@@ -0,0 +1,156 @@
+<cd-modal [modalRef]="activeModal">
+  <ng-container i18n="form title"
+                class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+  <ng-container class="modal-content">
+    <form name="subvolumeForm"
+          #formDir="ngForm"
+          [formGroup]="subvolumeForm"
+          novalidate>
+      <div class="modal-body">
+        <div class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="subvolumeName"
+                 i18n>Name</label>
+          <div class="cd-col-form-input">
+            <input class="form-control"
+                   type="text"
+                   placeholder="Subvolume name..."
+                   id="subvolumeName"
+                   name="subvolumeName"
+                   formControlName="subvolumeName"
+                   autofocus>
+            <span class="invalid-feedback"
+                  *ngIf="subvolumeForm.showError('subvolumeName', formDir, 'required')"
+                  i18n>This field is required.</span>
+            <span class="invalid-feedback"
+                  *ngIf="subvolumeForm.showError('subvolumeName', formDir, 'notUnique')"
+                  i18n>The subvolume already exists.</span>
+          </div>
+        </div>
+
+        <!-- Volume name -->
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="volumeName"
+                 i18n>Volume name</label>
+          <div class="cd-col-form-input">
+            <input class="form-control"
+                   id="volumeName"
+                   name="volumeName"
+                   formControlName="volumeName">
+          </div>
+        </div>
+
+        <!-- Size -->
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="size"
+                 i18n>Size
+          <cd-helper>The size of the subvolume is specified by setting a quota on it</cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <input class="form-control"
+                   type="text"
+                   id="size"
+                   name="size"
+                   formControlName="size"
+                   i18n-placeholder
+                   placeholder="e.g., 10GiB"
+                   defaultUnit="GiB"
+                   cdDimlessBinary>
+          </div>
+        </div>
+
+        <!-- CephFS Pools -->
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="pool"
+                 i18n>Pool
+            <cd-helper>By default, the data_pool_layout of the parent directory is selected.</cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <select class="form-select"
+                    id="pool"
+                    name="pool"
+                    formControlName="pool">
+              <option *ngFor="let pool of dataPools"
+                      [value]="pool.pool">{{ pool.pool }}</option>
+            </select>
+          </div>
+        </div>
+
+        <!-- UID -->
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="uid"
+                 i18n>UID</label>
+          <div class="cd-col-form-input">
+            <input class="form-control"
+                   type="number"
+                   placeholder="Subvolume UID..."
+                   id="uid"
+                   name="uid"
+                   formControlName="uid">
+          </div>
+        </div>
+
+        <!-- GID -->
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="gid"
+                 i18n>GID</label>
+          <div class="cd-col-form-input">
+            <input class="form-control"
+                   type="number"
+                   placeholder="Subvolume GID..."
+                   id="gid"
+                   name="gid"
+                   formControlName="gid">
+          </div>
+        </div>
+
+        <!-- Mode -->
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="mode"
+                 i18n>Mode
+            <cd-helper>Permissions for the directory. Default mode is 755 which is rwxr-xr-x</cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <cd-checked-table-form [data]="scopePermissions"
+                                   [columns]="columns"
+                                   [form]="subvolumeForm"
+                                   inputField="mode"
+                                   [isTableForOctalMode]="true"
+                                   [scopes]="scopes"></cd-checked-table-form>
+          </div>
+          </div>
+
+        <!-- Is namespace-isolated -->
+        <div class="form-group row">
+          <div class="cd-col-form-offset">
+            <div class="custom-control custom-checkbox">
+              <input class="custom-control-input"
+                     type="checkbox"
+                     id="isolatedNamespace"
+                     name="isolatedNamespace"
+                     formControlName="isolatedNamespace">
+              <label class="custom-control-label"
+                     for="isolatedNamespace"
+                     i18n>Isolated Namespace
+                <cd-helper>To create subvolume in a separate RADOS namespace.</cd-helper>
+              </label>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="modal-footer">
+        <cd-form-button-panel (submitActionEvent)="submit()"
+                              [form]="subvolumeForm"
+                              [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+      </div>
+    </form>
+  </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.spec.ts
new file mode 100644 (file)
index 0000000..9407290
--- /dev/null
@@ -0,0 +1,39 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CephfsSubvolumeFormComponent } from './cephfs-subvolume-form.component';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { SharedModule } from '~/app/shared/shared.module';
+import { RouterTestingModule } from '@angular/router/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+
+describe('CephfsSubvolumeFormComponent', () => {
+  let component: CephfsSubvolumeFormComponent;
+  let fixture: ComponentFixture<CephfsSubvolumeFormComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [CephfsSubvolumeFormComponent],
+      providers: [NgbActiveModal],
+      imports: [
+        SharedModule,
+        ToastrModule.forRoot(),
+        ReactiveFormsModule,
+        HttpClientTestingModule,
+        RouterTestingModule
+      ]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(CephfsSubvolumeFormComponent);
+    component = fixture.componentInstance;
+    component.pools = [];
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.ts
new file mode 100644 (file)
index 0000000..09d52da
--- /dev/null
@@ -0,0 +1,138 @@
+import { Component, OnInit } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { Pool } from '../../pool/pool';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import _ from 'lodash';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+
+@Component({
+  selector: 'cd-cephfs-subvolume-form',
+  templateUrl: './cephfs-subvolume-form.component.html',
+  styleUrls: ['./cephfs-subvolume-form.component.scss']
+})
+export class CephfsSubvolumeFormComponent implements OnInit {
+  fsName: string;
+  pools: Pool[];
+
+  subvolumeForm: CdFormGroup;
+
+  action: string;
+  resource: string;
+
+  dataPools: Pool[];
+
+  columns: CdTableColumn[];
+  scopePermissions: Array<any> = [];
+  scopes: string[] = ['owner', 'group', 'others'];
+
+  constructor(
+    public activeModal: NgbActiveModal,
+    private actionLabels: ActionLabelsI18n,
+    private taskWrapper: TaskWrapperService,
+    private cephFsSubvolumeService: CephfsSubvolumeService,
+    private formatter: FormatterService
+  ) {
+    this.action = this.actionLabels.CREATE;
+    this.resource = $localize`Subvolume`;
+  }
+
+  ngOnInit(): void {
+    this.columns = [
+      {
+        prop: 'scope',
+        name: $localize`All`,
+        flexGrow: 0.5
+      },
+      {
+        prop: 'read',
+        name: $localize`Read`,
+        flexGrow: 0.5,
+        cellClass: 'text-center'
+      },
+      {
+        prop: 'write',
+        name: $localize`Write`,
+        flexGrow: 0.5,
+        cellClass: 'text-center'
+      },
+      {
+        prop: 'execute',
+        name: $localize`Execute`,
+        flexGrow: 0.5,
+        cellClass: 'text-center'
+      }
+    ];
+
+    this.dataPools = this.pools.filter((pool) => pool.type === 'data');
+    this.createForm();
+  }
+
+  createForm() {
+    this.subvolumeForm = new CdFormGroup({
+      volumeName: new FormControl({ value: this.fsName, disabled: true }),
+      subvolumeName: new FormControl('', {
+        validators: [Validators.required],
+        asyncValidators: [
+          CdValidators.unique(
+            this.cephFsSubvolumeService.exists,
+            this.cephFsSubvolumeService,
+            null,
+            null,
+            this.fsName
+          )
+        ]
+      }),
+      pool: new FormControl(this.dataPools[0]?.pool, {
+        validators: [Validators.required]
+      }),
+      size: new FormControl(null, {
+        updateOn: 'blur'
+      }),
+      uid: new FormControl(null),
+      gid: new FormControl(null),
+      mode: new FormControl({}),
+      isolatedNamespace: new FormControl(false)
+    });
+  }
+
+  submit() {
+    const subVolumeName = this.subvolumeForm.getValue('subvolumeName');
+    const pool = this.subvolumeForm.getValue('pool');
+    const size = this.formatter.toBytes(this.subvolumeForm.getValue('size'));
+    const uid = this.subvolumeForm.getValue('uid');
+    const gid = this.subvolumeForm.getValue('gid');
+    const mode = this.formatter.toOctalPermission(this.subvolumeForm.getValue('mode'));
+    const isolatedNamespace = this.subvolumeForm.getValue('isolatedNamespace');
+    this.taskWrapper
+      .wrapTaskAroundCall({
+        task: new FinishedTask('cephfs/subvolume/' + URLVerbs.CREATE, {
+          subVolumeName: subVolumeName
+        }),
+        call: this.cephFsSubvolumeService.create(
+          this.fsName,
+          subVolumeName,
+          pool,
+          size,
+          uid,
+          gid,
+          mode,
+          isolatedNamespace
+        )
+      })
+      .subscribe({
+        error: () => {
+          this.subvolumeForm.setErrors({ cdSubmitButton: true });
+        },
+        complete: () => {
+          this.activeModal.close();
+        }
+      });
+  }
+}
index 7ecb3faae8013fe8add94393c307af6a1bba5d5a..53aa454e4cb6a6cc91c449c763bd01128bc8500c 100644 (file)
@@ -3,7 +3,17 @@
             columnMode="flex"
             [columns]="columns"
             selectionType="single"
-            [hasDetails]="false">
+            [hasDetails]="false"
+            (fetchData)="fetchData()">
+
+    <div class="table-actions btn-toolbar">
+      <cd-table-actions [permission]="permissions.cephfs"
+                        [selection]="selection"
+                        class="btn-group"
+                        id="cephfs-subvolume-actions"
+                        [tableActions]="tableActions">
+      </cd-table-actions>
+    </div>
   </cd-table>
 </ng-container>
 
index 59fc488890563329c73cc2318e107451b2fc9678..14c0ea724da604eb596bceac3055926b35a06dad 100644 (file)
@@ -1,13 +1,19 @@
 import { Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
-import { Observable, of } from 'rxjs';
-import { catchError } from 'rxjs/operators';
+import { Observable, ReplaySubject, of } from 'rxjs';
+import { catchError, shareReplay, switchMap } from 'rxjs/operators';
 import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
 import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
 import { Icons } from '~/app/shared/enum/icons.enum';
+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 { CdTableSelection } from '~/app/shared/models/cd-table-selection';
 import { CephfsSubvolume } from '~/app/shared/models/cephfs-subvolume.model';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { CephfsSubvolumeFormComponent } from '../cephfs-subvolume-form/cephfs-subvolume-form.component';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { Permissions } from '~/app/shared/models/permissions';
 
 @Component({
   selector: 'cd-cephfs-subvolume-list',
@@ -31,15 +37,26 @@ export class CephfsSubvolumeListComponent implements OnInit, OnChanges {
   quotaSizeTpl: any;
 
   @Input() fsName: string;
+  @Input() pools: any[];
 
   columns: CdTableColumn[] = [];
+  tableActions: CdTableAction[];
   context: CdTableFetchDataContext;
   selection = new CdTableSelection();
   icons = Icons;
+  permissions: Permissions;
 
   subVolumes$: Observable<CephfsSubvolume[]>;
+  subject = new ReplaySubject<CephfsSubvolume[]>();
 
-  constructor(private cephfsSubVolume: CephfsSubvolumeService) {}
+  constructor(
+    private cephfsSubVolume: CephfsSubvolumeService,
+    private actionLabels: ActionLabelsI18n,
+    private modalService: ModalService,
+    private authStorageService: AuthStorageService
+  ) {
+    this.permissions = this.authStorageService.getPermissions();
+  }
 
   ngOnInit(): void {
     this.columns = [
@@ -84,15 +101,43 @@ export class CephfsSubvolumeListComponent implements OnInit, OnChanges {
         cellTransformation: CellTemplate.timeAgo
       }
     ];
+
+    this.tableActions = [
+      {
+        name: this.actionLabels.CREATE,
+        permission: 'create',
+        icon: Icons.add,
+        click: () =>
+          this.modalService.show(
+            CephfsSubvolumeFormComponent,
+            {
+              fsName: this.fsName,
+              pools: this.pools
+            },
+            { size: 'lg' }
+          )
+      }
+    ];
+
+    this.subVolumes$ = this.subject.pipe(
+      switchMap(() =>
+        this.cephfsSubVolume.get(this.fsName).pipe(
+          catchError(() => {
+            this.context.error();
+            return of(null);
+          })
+        )
+      ),
+      shareReplay(1)
+    );
+  }
+
+  fetchData() {
+    this.subject.next();
   }
 
   ngOnChanges() {
-    this.subVolumes$ = this.cephfsSubVolume.get(this.fsName).pipe(
-      catchError(() => {
-        this.context.error();
-        return of(null);
-      })
-    );
+    this.subject.next();
   }
 
   updateSelection(selection: CdTableSelection) {
index 50545a1ad65cea20a68305213afdd7c0aaed5b37..8d49a74dfb8a11823c104eef4793dab026207b66 100644 (file)
@@ -16,7 +16,8 @@
       <a ngbNavLink
          i18n>Subvolumes</a>
       <ng-template ngbNavContent>
-        <cd-cephfs-subvolume-list [fsName]="selection.mdsmap.fs_name"></cd-cephfs-subvolume-list>
+        <cd-cephfs-subvolume-list [fsName]="selection.mdsmap.fs_name"
+                                  [pools]="details.pools"></cd-cephfs-subvolume-list>
       </ng-template>
     </ng-container>
     <ng-container ngbNavItem="clients">
index 35563456623478ac25e55b3e5da763f6f6029109..892c9058655b1eaab1f0ed8e6ef142835e4422c9 100644 (file)
@@ -16,6 +16,7 @@ import { CephfsVolumeFormComponent } from './cephfs-form/cephfs-form.component';
 import { CephfsListComponent } from './cephfs-list/cephfs-list.component';
 import { CephfsTabsComponent } from './cephfs-tabs/cephfs-tabs.component';
 import { CephfsSubvolumeListComponent } from './cephfs-subvolume-list/cephfs-subvolume-list.component';
+import { CephfsSubvolumeFormComponent } from './cephfs-subvolume-form/cephfs-subvolume-form.component';
 
 @NgModule({
   imports: [
@@ -38,7 +39,8 @@ import { CephfsSubvolumeListComponent } from './cephfs-subvolume-list/cephfs-sub
     CephfsTabsComponent,
     CephfsVolumeFormComponent,
     CephfsDirectoriesComponent,
-    CephfsSubvolumeListComponent
+    CephfsSubvolumeListComponent,
+    CephfsSubvolumeFormComponent
   ]
 })
 export class CephfsModule {}
index e7876d29464e0d47c9144b5830e5acb3639d7e23..61079945e043152aa4fcccc86b231c54364f733e 100644 (file)
@@ -23,7 +23,6 @@ import { RoleFormModel } from './role-form.model';
   styleUrls: ['./role-form.component.scss']
 })
 export class RoleFormComponent extends CdForm implements OnInit {
-
   roleForm: CdFormGroup;
   response: RoleFormModel;
 
index a983f7e2c7a847fb8d472e683d611123cfd7befe..bd9a16e0e333baf5b5699e2636c947dc9bfcaeb7 100644 (file)
@@ -1,7 +1,9 @@
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 import { CephfsSubvolume } from '../models/cephfs-subvolume.model';
-import { Observable } from 'rxjs';
+import { Observable, of } from 'rxjs';
+import { catchError, mapTo } from 'rxjs/operators';
+import _ from 'lodash';
 
 @Injectable({
   providedIn: 'root'
@@ -14,4 +16,50 @@ export class CephfsSubvolumeService {
   get(fsName: string): Observable<CephfsSubvolume[]> {
     return this.http.get<CephfsSubvolume[]>(`${this.baseURL}/${fsName}`);
   }
+
+  create(
+    fsName: string,
+    subVolumeName: string,
+    poolName: string,
+    size: number,
+    uid: number,
+    gid: number,
+    mode: string,
+    namespace: boolean
+  ) {
+    return this.http.post(
+      this.baseURL,
+      {
+        vol_name: fsName,
+        subvol_name: subVolumeName,
+        pool_layout: poolName,
+        size: size,
+        uid: uid,
+        gid: gid,
+        mode: mode,
+        namespace_isolated: namespace
+      },
+      { observe: 'response' }
+    );
+  }
+
+  info(fsName: string, subVolumeName: string) {
+    return this.http.get(`${this.baseURL}/${fsName}/info`, {
+      params: {
+        subvol_name: subVolumeName
+      }
+    });
+  }
+
+  exists(subVolumeName: string, fsName: string) {
+    return this.info(fsName, subVolumeName).pipe(
+      mapTo(true),
+      catchError((error: Event) => {
+        if (_.isFunction(error.preventDefault)) {
+          error.preventDefault();
+        }
+        return of(false);
+      })
+    );
+  }
 }
index 4e212adeba994d566d6552a984a195b6b8b983ea..fb5c9e8120b0d1b9b162f85b233ed1ed304d51f4 100644 (file)
@@ -83,4 +83,8 @@ export class CephfsService {
       }
     );
   }
+
+  isCephFsPool(pool: any) {
+    return _.indexOf(pool.application_metadata, 'cephfs') !== -1 && !pool.pool_name.includes('/');
+  }
 }
index 22371a50f71ecd0b85b65dd54842443c54a5ee65..bea426724e0736d396dac2f50048b0ca9dc85a5c 100644 (file)
@@ -18,7 +18,7 @@ export function isEmptyInputValue(value: any): boolean {
   return value == null || value.length === 0;
 }
 
-export type existsServiceFn = (value: any) => Observable<boolean>;
+export type existsServiceFn = (value: any, args?: any) => Observable<boolean>;
 
 export class CdValidators {
   /**
@@ -358,7 +358,8 @@ export class CdValidators {
     serviceFn: existsServiceFn,
     serviceFnThis: any = null,
     usernameFn?: Function,
-    uidField = false
+    uidField = false,
+    extraArgs = ''
   ): AsyncValidatorFn {
     let uName: string;
     return (control: AbstractControl): Observable<ValidationErrors | null> => {
@@ -377,7 +378,7 @@ export class CdValidators {
       }
 
       return observableTimer().pipe(
-        switchMapTo(serviceFn.call(serviceFnThis, uName)),
+        switchMapTo(serviceFn.call(serviceFnThis, uName, extraArgs)),
         map((resp: boolean) => {
           if (!resp) {
             return null;
index eacba3cf16dccddce9bf0529fef024d216d86845..b5e0b9475a44533830fd71ec69d49886434074cf 100644 (file)
@@ -120,4 +120,22 @@ export class FormatterService {
 
     return 0;
   }
+
+  toOctalPermission(modes: any) {
+    const scopes = ['owner', 'group', 'others'];
+    let octalMode = '';
+    for (const scope of scopes) {
+      let scopeValue = 0;
+      const mode = modes[scope];
+
+      if (mode) {
+        if (mode.includes('read')) scopeValue += 4;
+        if (mode.includes('write')) scopeValue += 2;
+        if (mode.includes('execute')) scopeValue += 1;
+      }
+
+      octalMode += scopeValue.toString();
+    }
+    return octalMode;
+  }
 }
index bf8f189b4e497a1c81660ce591d818abe2a6fb2b..fc5b08be8bd81748d13eae18d7cb0b3821570115 100644 (file)
@@ -355,6 +355,9 @@ export class TaskMessageService {
     ),
     'cephfs/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
       this.volume(metadata)
+    ),
+    'cephfs/subvolume/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+      this.subvolume(metadata)
     )
   };
 
@@ -415,6 +418,10 @@ export class TaskMessageService {
     return $localize`'${metadata.volumeName}'`;
   }
 
+  subvolume(metadata: any) {
+    return $localize`subvolume '${metadata.subVolumeName}'`;
+  }
+
   crudMessageId(id: string) {
     return $localize`${id}`;
   }
index 8afa6a4ba9e9390146ca7098f13224684a0ad935..c8431383e9413ae2b4cd9397659fd3066d74c460 100644 (file)
@@ -1681,6 +1681,46 @@ paths:
       - jwt: []
       tags:
       - Cephfs
+  /api/cephfs/subvolume:
+    post:
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                subvol_name:
+                  type: string
+                vol_name:
+                  type: string
+              required:
+              - vol_name
+              - subvol_name
+              type: object
+      responses:
+        '201':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              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: []
+      tags:
+      - CephFSSubvolume
   /api/cephfs/subvolume/{vol_name}:
     get:
       parameters:
@@ -1708,6 +1748,38 @@ paths:
       - jwt: []
       tags:
       - CephFSSubvolume
+  /api/cephfs/subvolume/{vol_name}/info:
+    get:
+      parameters:
+      - in: path
+        name: vol_name
+        required: true
+        schema:
+          type: string
+      - in: query
+        name: subvol_name
+        required: true
+        schema:
+          type: string
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: OK
+        '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: []
+      tags:
+      - CephFSSubvolume
   /api/cephfs/{fs_id}:
     get:
       parameters: