]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: allow managing pool quotas in frontend 27945/head
authorKiefer Chang <kiefer.chang@suse.com>
Fri, 10 May 2019 03:54:12 +0000 (11:54 +0800)
committerKiefer Chang <kiefer.chang@suse.com>
Tue, 18 Jun 2019 07:44:31 +0000 (15:44 +0800)
Fixes: https://tracker.ceph.com/issues/36559
Signed-off-by: Kiefer Chang <kiefer.chang@suse.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts

index ad9dca78e350ea1776af275923722a3afeb3a288..5b8ce50800936df8227485594930d891a3c96c13 100644 (file)
             </div>
           </div>
 
+        <!-- Quotas -->
+        <div>
+          <legend i18n>Quotas</legend>
+
+          <!-- Max Bytes -->
+          <div class="form-group"
+               [ngClass]="{'has-error': form.showError('max_bytes', formDir)}">
+            <label class="control-label col-sm-3"
+                   for="max_bytes">
+              <ng-container i18n>Max bytes</ng-container>
+              <cd-helper>
+                <span i18n>Leave it blank or specify 0 to disable this quota.</span>
+                <br>
+                <span i18n>A valid quota should be greater than 0.</span>
+              </cd-helper>
+            </label>
+            <div class="col-sm-9">
+              <input class="form-control"
+                     id="max_bytes"
+                     name="max_bytes"
+                     type="text"
+                     formControlName="max_bytes"
+                     i18n-placeholder
+                     placeholder="e.g., 10GiB"
+                     defaultUnit="GiB"
+                     cdDimlessBinary>
+            </div>
+          </div>
+
+          <!-- Max Objects -->
+          <div class="form-group"
+               [ngClass]="{'has-error': form.showError('max_objects', formDir)}">
+            <label class="control-label col-sm-3"
+                   for="max_objects">
+              <ng-container i18n>Max objects</ng-container>
+              <cd-helper>
+                <span i18n>Leave it blank or specify 0 to disable this quota.</span>
+                <br>
+                <span i18n>A valid quota should be greater than 0.</span>
+              </cd-helper>
+            </label>
+            <div class="col-sm-9">
+              <input class="form-control"
+                     id="max_objects"
+                     min="0"
+                     name="max_objects"
+                     type="number"
+                     formControlName="max_objects">
+              <span class="help-block"
+                    *ngIf="form.showError('max_objects', formDir, 'min')"
+                    i18n>The value should be greater or equal to 0</span>
+            </div>
+          </div>
+        </div>
+
         <!-- Pool configuration -->
         <div [hidden]="form.get('poolType').value !== 'replicated' || data.applications.selected.indexOf('rbd') === -1">
           <cd-rbd-configuration-form [form]="form"
index 51e4e3f1ed5ea6823a000b60ab6e82309e499db9..87f18df8c988fc3c183654d288cbbc66ca2ee860 100644 (file)
@@ -302,6 +302,15 @@ describe('PoolFormComponent', () => {
       expect(form.getValue('mode')).toBe('none');
     });
 
+    it('validate quotas', () => {
+      formHelper.expectValid('max_bytes');
+      formHelper.expectValid('max_objects');
+      formHelper.expectValidChange('max_bytes', '10 Gib');
+      formHelper.expectValidChange('max_bytes', '');
+      formHelper.expectValidChange('max_objects', '');
+      formHelper.expectErrorChange('max_objects', -1, 'min');
+    });
+
     describe('compression form', () => {
       beforeEach(() => {
         formHelper.setValue('poolType', 'replicated');
@@ -930,6 +939,23 @@ describe('PoolFormComponent', () => {
           size: 2
         });
       });
+
+      it('with quotas', () => {
+        setMultipleValues({
+          name: 'RepPoolWithQuotas',
+          poolType: 'replicated',
+          max_bytes: 1024 * 1024,
+          max_objects: 3000,
+          pgNum: 8
+        });
+        testCreate({
+          pool: 'RepPoolWithQuotas',
+          pool_type: 'replicated',
+          quota_max_bytes: 1024 * 1024,
+          quota_max_objects: 3000,
+          pg_num: 8
+        });
+      });
     });
 
     it('pool with compression', () => {
@@ -992,6 +1018,8 @@ describe('PoolFormComponent', () => {
       pool.options.compression_required_ratio = 0.8;
       pool.flags_names = 'someFlag1,someFlag2';
       pool.application_metadata = ['rbd', 'rgw'];
+      pool.quota_max_bytes = 1024 * 1024 * 1024;
+      pool.quota_max_objects = 3000;
 
       createCrushRule({ name: 'someRule' });
       spyOn(poolService, 'get').and.callFake(() => of(pool));
@@ -1025,7 +1053,9 @@ describe('PoolFormComponent', () => {
           'algorithm',
           'minBlobSize',
           'maxBlobSize',
-          'ratio'
+          'ratio',
+          'max_bytes',
+          'max_objects'
         ];
         enabled.forEach((controlName) => {
           return expect(form.get(controlName).enabled).toBeTruthy();
@@ -1043,6 +1073,8 @@ describe('PoolFormComponent', () => {
         expect(form.getValue('minBlobSize')).toBe('512 KiB');
         expect(form.getValue('maxBlobSize')).toBe('1 MiB');
         expect(form.getValue('ratio')).toBe(pool.options.compression_required_ratio);
+        expect(form.getValue('max_bytes')).toBe('1 GiB');
+        expect(form.getValue('max_objects')).toBe(pool.quota_max_objects);
       });
 
       it('updates pgs on every change', () => {
index b6f57ee9a8266358101bdafdb9b7ef834e0ca9e8..52ac901765651d66a7a51af75575618539f27b89 100644 (file)
@@ -136,7 +136,11 @@ export class PoolFormComponent implements OnInit {
           validators: [Validators.required, Validators.min(1)]
         }),
         ecOverwrites: new FormControl(false),
-        compression: compressionForm
+        compression: compressionForm,
+        max_bytes: new FormControl(''),
+        max_objects: new FormControl(0, {
+          validators: [Validators.min(0)]
+        })
       },
       [
         CdValidators.custom('form', () => null),
@@ -221,7 +225,9 @@ export class PoolFormComponent implements OnInit {
       algorithm: pool.options.compression_algorithm,
       minBlobSize: this.dimlessBinaryPipe.transform(pool.options.compression_min_blob_size),
       maxBlobSize: this.dimlessBinaryPipe.transform(pool.options.compression_max_blob_size),
-      ratio: pool.options.compression_required_ratio
+      ratio: pool.options.compression_required_ratio,
+      max_bytes: this.dimlessBinaryPipe.transform(pool.quota_max_bytes),
+      max_objects: pool.quota_max_objects
     };
 
     Object.keys(dataMap).forEach((controlName: string) => {
@@ -529,7 +535,20 @@ export class PoolFormComponent implements OnInit {
             formControlName: 'erasureProfile',
             attr: 'name'
           },
-      { externalFieldName: 'rule_name', formControlName: 'crushRule', attr: 'rule_name' }
+      { externalFieldName: 'rule_name', formControlName: 'crushRule', attr: 'rule_name' },
+      {
+        externalFieldName: 'quota_max_bytes',
+        formControlName: 'max_bytes',
+        replaceFn: this.formatter.toBytes,
+        editable: true,
+        resetValue: this.editing ? 0 : undefined
+      },
+      {
+        externalFieldName: 'quota_max_objects',
+        formControlName: 'max_objects',
+        editable: true,
+        resetValue: this.editing ? 0 : undefined
+      }
     ]);
 
     if (this.info.is_all_bluestore) {