]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: enable/disable versioning on RGW bucket 29460/head
authorAlfonso Martínez <almartin@redhat.com>
Thu, 26 Sep 2019 06:41:14 +0000 (08:41 +0200)
committerAlfonso Martínez <almartin@redhat.com>
Thu, 26 Sep 2019 06:41:14 +0000 (08:41 +0200)
Fixes: https://tracker.ceph.com/issues/40920
Signed-off-by: Alfonso Martínez <almartin@redhat.com>
qa/tasks/mgr/dashboard/test_rgw.py
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.po.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts
src/pybind/mgr/dashboard/services/rgw_client.py

index ad6b4b8cad7f5ef6ea20118e92d52f260a0b8c07..a04ee7fc6445e75ecadcaf925fcc538a821fedd8 100644 (file)
@@ -113,16 +113,22 @@ class RgwBucketTest(RgwTestCase):
     def setUpClass(cls):
         cls.create_test_user = True
         super(RgwBucketTest, cls).setUpClass()
-        # Create a tenanted user.
+        # Create tenanted users.
         cls._radosgw_admin_cmd([
             'user', 'create', '--tenant', 'testx', '--uid', 'teuth-test-user',
             '--display-name', 'tenanted teuth-test-user'
         ])
+        cls._radosgw_admin_cmd([
+            'user', 'create', '--tenant', 'testx2', '--uid', 'teuth-test-user2',
+            '--display-name', 'tenanted teuth-test-user 2'
+        ])
 
     @classmethod
     def tearDownClass(cls):
         cls._radosgw_admin_cmd(
             ['user', 'rm', '--tenant', 'testx', '--uid=teuth-test-user'])
+        cls._radosgw_admin_cmd(
+            ['user', 'rm', '--tenant', 'testx2', '--uid=teuth-test-user2'])
         super(RgwBucketTest, cls).tearDownClass()
 
     def test_all(self):
@@ -171,13 +177,16 @@ class RgwBucketTest(RgwTestCase):
         }, allow_unknown=True))
         self.assertEqual(data['bucket'], 'teuth-test-bucket')
         self.assertEqual(data['owner'], 'admin')
+        self.assertEqual(data['placement_rule'], 'default-placement')
+        self.assertEqual(data['versioning'], 'Suspended')
 
         # Update the bucket.
         self._put(
             '/api/rgw/bucket/teuth-test-bucket',
             params={
                 'bucket_id': data['id'],
-                'uid': 'teuth-test-user'
+                'uid': 'teuth-test-user',
+                'versioning_state': 'Enabled'
             })
         self.assertStatus(200)
         data = self._get('/api/rgw/bucket/teuth-test-bucket')
@@ -188,6 +197,7 @@ class RgwBucketTest(RgwTestCase):
             'tenant': JLeaf(str)
         }, allow_unknown=True))
         self.assertEqual(data['owner'], 'teuth-test-user')
+        self.assertEqual(data['versioning'], 'Enabled')
 
         # Delete the bucket.
         self._delete('/api/rgw/bucket/teuth-test-bucket')
@@ -239,34 +249,50 @@ class RgwBucketTest(RgwTestCase):
 
         # Get the bucket.
         data = _verify_tenant_bucket('teuth-test-bucket', 'testx', 'teuth-test-user')
+        self.assertEqual(data['placement_rule'], 'default-placement')
+        self.assertEqual(data['versioning'], 'Suspended')
 
-        # Change owner to a non-tenanted user
+        # Update bucket: different user with different tenant, enable versioning.
         self._put(
             '/api/rgw/bucket/{}'.format(
                 urllib.quote_plus('testx/teuth-test-bucket')),
+            params={
+                'bucket_id': data['id'],
+                'uid': 'testx2$teuth-test-user2',
+                'versioning_state': 'Enabled'
+            })
+        data = _verify_tenant_bucket('teuth-test-bucket', 'testx2', 'teuth-test-user2')
+        self.assertEqual(data['versioning'], 'Enabled')
+
+        # Change owner to a non-tenanted user
+        self._put(
+            '/api/rgw/bucket/{}'.format(
+                urllib.quote_plus('testx2/teuth-test-bucket')),
             params={
                 'bucket_id': data['id'],
                 'uid': 'admin'
             })
         self.assertStatus(200)
-        data = self._get('/api/rgw/bucket/{}'.format(
-            urllib.quote_plus('teuth-test-bucket')))
+        data = self._get('/api/rgw/bucket/teuth-test-bucket')
         self.assertStatus(200)
         self.assertIn('owner', data)
         self.assertEqual(data['owner'], 'admin')
         self.assertEqual(data['tenant'], '')
         self.assertEqual(data['bucket'], 'teuth-test-bucket')
         self.assertEqual(data['bid'], 'teuth-test-bucket')
+        self.assertEqual(data['versioning'], 'Enabled')
 
-        # Change owner back to tenanted user
+        # Change owner back to tenanted user, suspend versioning.
         self._put(
             '/api/rgw/bucket/teuth-test-bucket',
             params={
                 'bucket_id': data['id'],
-                'uid': 'testx$teuth-test-user'
+                'uid': 'testx$teuth-test-user',
+                'versioning_state': 'Suspended'
             })
         self.assertStatus(200)
         data = _verify_tenant_bucket('teuth-test-bucket', 'testx', 'teuth-test-user')
+        self.assertEqual(data['versioning'], 'Suspended')
 
         # Delete the bucket.
         self._delete('/api/rgw/bucket/{}'.format(
index e355503780f231b4c674e7a48d50efba475ee309..f743099c0c365f07fb2212f8dc6651cbe5717911 100644 (file)
@@ -137,11 +137,51 @@ class RgwBucket(RgwRESTController):
                 if bucket['tenant'] else bucket['bucket']
         return bucket
 
+    def _get_versioning(self, owner, bucket_name):
+        rgw_client = RgwClient.instance(owner)
+        return rgw_client.get_bucket_versioning(bucket_name)
+
+    def _set_versioning(self, owner, bucket_name, versioning_state):
+        rgw_client = RgwClient.instance(owner)
+        return rgw_client.set_bucket_versioning(bucket_name, versioning_state)
+
+    @staticmethod
+    def strip_tenant_from_bucket_name(bucket_name):
+        # type (str) => str
+        """
+        >>> RgwBucket.strip_tenant_from_bucket_name('tenant/bucket-name')
+        'bucket-name'
+        >>> RgwBucket.strip_tenant_from_bucket_name('bucket-name')
+        'bucket-name'
+        """
+        return bucket_name[bucket_name.find('/') + 1:]
+
+    @staticmethod
+    def get_s3_bucket_name(bucket_name, tenant=None):
+        # type (str, str) => str
+        """
+        >>> RgwBucket.get_s3_bucket_name('bucket-name', 'tenant')
+        'tenant:bucket-name'
+        >>> RgwBucket.get_s3_bucket_name('tenant/bucket-name', 'tenant')
+        'tenant:bucket-name'
+        >>> RgwBucket.get_s3_bucket_name('bucket-name')
+        'bucket-name'
+        """
+        bucket_name = RgwBucket.strip_tenant_from_bucket_name(bucket_name)
+        if tenant:
+            bucket_name = '{}:{}'.format(tenant, bucket_name)
+        return bucket_name
+
     def list(self):
         return self.proxy('GET', 'bucket')
 
     def get(self, bucket):
         result = self.proxy('GET', 'bucket', {'bucket': bucket})
+
+        result['versioning'] =\
+            self._get_versioning(result['owner'],
+                                 RgwBucket.get_s3_bucket_name(result['bucket'], result['tenant']))
+
         return self._append_bid(result)
 
     def create(self, bucket, uid, zonegroup=None, placement_target=None):
@@ -151,16 +191,25 @@ class RgwBucket(RgwRESTController):
         except RequestException as e:
             raise DashboardException(e, http_status_code=500, component='rgw')
 
-    def set(self, bucket, bucket_id, uid):
+    def set(self, bucket, bucket_id, uid, versioning_state=None):
         # When linking a non-tenant-user owned bucket to a tenanted user, we
         # need to prefix bucket name with '/'. e.g. photos -> /photos
         if '$' in uid and '/' not in bucket:
             bucket = '/{}'.format(bucket)
+
+        # Link bucket to new user:
         result = self.proxy('PUT', 'bucket', {
             'bucket': bucket,
             'bucket-id': bucket_id,
             'uid': uid
         }, json_response=False)
+
+        if versioning_state:
+            uid_tenant = uid[:uid.find('$')] if uid.find('$') >= 0 else None
+            self._set_versioning(uid,
+                                 RgwBucket.get_s3_bucket_name(bucket, uid_tenant),
+                                 versioning_state)
+
         return self._append_bid(result)
 
     def delete(self, bucket, purge_objects='true'):
index 6a11c92f25a468afb03f86050ba019dbf6676185..b2cd3c45ad62f8afcfe45d010115adcdeac69a86 100644 (file)
@@ -8,6 +8,8 @@ const pages = {
 
 export class BucketsPageHelper extends PageHelper {
   pages = pages;
+  versioningStateEnabled = 'Enabled';
+  versioningStateSuspended = 'Suspended';
 
   /**
    * TODO add check to verify the existance of the bucket!
@@ -50,6 +52,16 @@ export class BucketsPageHelper extends PageHelper {
     );
     await element(by.id('owner')).click(); // click owner dropdown menu
     await element(by.cssContainingText('select[name=owner] option', new_owner)).click(); // select the new user
+
+    // Enable versioning
+    await expect(element(by.css('input[name=versioning]:checked')).getAttribute('value')).toBe(
+      this.versioningStateSuspended
+    );
+    await element(by.css('input[id=enabled]')).click();
+    await expect(element(by.css('input[name=versioning]:checked')).getAttribute('value')).toBe(
+      this.versioningStateEnabled
+    );
+
     await element(by.cssContainingText('button', 'Edit Bucket')).click();
 
     // wait to be back on buckets page with table visible and click
@@ -59,10 +71,45 @@ export class BucketsPageHelper extends PageHelper {
     );
 
     // check its details table for edited owner field
-    const element_details_table = element
-      .all(by.css('.table.table-striped.table-bordered'))
-      .first();
-    return expect(element_details_table.getText()).toMatch(new_owner);
+    let bucketDataTable = element.all(by.css('.table.table-striped.table-bordered')).first();
+    await expect(bucketDataTable.getText()).toMatch(new_owner);
+
+    // Check versioning enabled:
+    const ownerValueCell = bucketDataTable
+      .all(by.css('tr'))
+      .get(2)
+      .all(by.css('td'))
+      .last();
+    await expect(ownerValueCell.getText()).toEqual(new_owner);
+    let versioningValueCell = bucketDataTable
+      .all(by.css('tr'))
+      .get(11)
+      .all(by.css('td'))
+      .last();
+    await expect(versioningValueCell.getText()).toEqual(this.versioningStateEnabled);
+
+    // Disable versioning:
+    await this.getFirstTableCellWithText(name).click(); // click on the bucket you want to edit in the table
+    await element(by.cssContainingText('button', 'Edit')).click(); // click button to move to edit page
+    await this.waitTextToBePresent(this.getBreadcrumb(), 'Edit');
+    await element(by.css('input[id=suspended]')).click();
+    await expect(element(by.css('input[name=versioning]:checked')).getAttribute('value')).toBe(
+      this.versioningStateSuspended
+    );
+    await element(by.cssContainingText('button', 'Edit Bucket')).click();
+
+    // Check versioning suspended:
+    await this.waitClickableAndClick(
+      this.getFirstTableCellWithText(name),
+      'Could not return to buckets page and load table after editing bucket'
+    );
+    bucketDataTable = element.all(by.css('.table.table-striped.table-bordered')).first();
+    versioningValueCell = bucketDataTable
+      .all(by.css('tr'))
+      .get(11)
+      .all(by.css('td'))
+      .last();
+    return expect(versioningValueCell.getText()).toEqual(this.versioningStateSuspended);
   }
 
   async testInvalidCreate() {
@@ -145,6 +192,10 @@ export class BucketsPageHelper extends PageHelper {
 
     await this.waitTextToBePresent(this.getBreadcrumb(), 'Edit');
 
+    await expect(element(by.css('input[name=versioning]:checked')).getAttribute('value')).toBe(
+      this.versioningStateSuspended
+    );
+
     // Chooses 'Select a user' rather than a valid owner on Edit Bucket page
     // and checks if it's an invalid input
     const ownerDropDown = element(by.id('owner'));
index f54277de110338186d586343851fd34e64e3cedc..d4ab903dc42076748ba3723f451a348bee77c5f3 100644 (file)
                 class="bold">Zonegroup</td>
             <td>{{ bucket.zonegroup }}</td>
           </tr>
+          <tr>
+            <td i18n
+                class="bold">Versioning</td>
+            <td>{{ bucket.versioning }}</td>
+          </tr>
         </tbody>
       </table>
 
index ce120fbf600942dfb977696d973c32d88e63b727..8182e61f63d6868e6992673685ed02d2e218dac0 100644 (file)
           </div>
         </div>
 
+        <!-- Versioning -->
+        <div class="form-group row"
+             *ngIf="editing">
+          <legend class="cd-header ml-5 mr-5" i18n>Versioning</legend>
+          <div class="col-sm-9 offset-sm-3">
+            <input type="radio"
+                   id="enabled"
+                   name="versioning"
+                   formControlName="versioning"
+                   value="Enabled"
+                   [checked]="bucketForm.get('versioning').value == 'Enabled'"
+                   class="custom-control custom-radio custom-control-inline align-top">
+            <label class="align-text-top"
+                   for="enabled">
+              <span i18n>Enabled</span>
+              <div class="text-muted" i18n>Enables versioning for the objects in the bucket.</div>
+            </label>
+          </div>
+          <div class="col-sm-9 offset-sm-3">
+            <input type="radio"
+                   id="suspended"
+                   name="versioning"
+                   formControlName="versioning"
+                   value="Suspended"
+                   [checked]="bucketForm.get('versioning').value != 'Enabled'"
+                   class="custom-control custom-radio custom-control-inline align-top">
+            <label class="align-text-top"
+                   for="suspended">
+              <span i18n>Suspended</span>
+              <div class="text-muted" i18n>Disables versioning for the objects in the bucket.</div>
+            </label>
+          <span class="invalid-feedback"
+                *ngIf="bucketForm.showError('versioning', frm, 'required')"
+                i18n>This field is required.</span>
+          </div>
+        </div>
+
       </div>
       <div class="card-footer">
         <div class="button-group text-right">
index 11c0c0e274b3f47c4ae874a244f0b31ceb02214c..8d3fec0cdfd62d0fda2631d0f4024191b639e44e 100644 (file)
@@ -169,7 +169,7 @@ describe('RgwBucketFormComponent', () => {
       component.submit();
       expect(notificationService.show).toHaveBeenCalledWith(
         NotificationType.success,
-        'Updated Object Gateway bucket ""'
+        'Updated Object Gateway bucket "".'
       );
     });
   });
index 356b63eb5cbc7acb42446bfc3328ebe85583b597..c9e0b47fe3182e777bb01cc3ce5652095ceb91c6 100644 (file)
@@ -52,7 +52,8 @@ export class RgwBucketFormComponent implements OnInit {
       id: [null],
       bid: [null, [Validators.required], this.editing ? [] : [this.bucketNameValidator()]],
       owner: [null, [Validators.required]],
-      'placement-target': [null, this.editing ? [] : [Validators.required]]
+      'placement-target': [null, this.editing ? [] : [Validators.required]],
+      versioning: [null, this.editing ? [Validators.required] : []]
     });
   }
 
@@ -124,19 +125,22 @@ export class RgwBucketFormComponent implements OnInit {
     if (this.editing) {
       // Edit
       const idCtl = this.bucketForm.get('id');
-      this.rgwBucketService.update(bidCtl.value, idCtl.value, ownerCtl.value).subscribe(
-        () => {
-          this.notificationService.show(
-            NotificationType.success,
-            this.i18n('Updated Object Gateway bucket "{{bid}}"', { bid: bidCtl.value })
-          );
-          this.goToListView();
-        },
-        () => {
-          // Reset the 'Submit' button.
-          this.bucketForm.setErrors({ cdSubmitButton: true });
-        }
-      );
+      const versioningCtl = this.bucketForm.get('versioning');
+      this.rgwBucketService
+        .update(bidCtl.value, idCtl.value, ownerCtl.value, versioningCtl.value)
+        .subscribe(
+          () => {
+            this.notificationService.show(
+              NotificationType.success,
+              this.i18n('Updated Object Gateway bucket "{{bid}}".', { bid: bidCtl.value })
+            );
+            this.goToListView();
+          },
+          () => {
+            // Reset the 'Submit' button.
+            this.bucketForm.setErrors({ cdSubmitButton: true });
+          }
+        );
     } else {
       // Add
       this.rgwBucketService
index b6c6f73154848f425049cf3577bdd4227f92de19..ff789e44feed6d91ca7c7d64822300decf05d7e5 100644 (file)
@@ -70,8 +70,10 @@ describe('RgwBucketService', () => {
   });
 
   it('should call update', () => {
-    service.update('foo', 'bar', 'baz').subscribe();
-    const req = httpTesting.expectOne('api/rgw/bucket/foo?bucket_id=bar&uid=baz');
+    service.update('foo', 'bar', 'baz', 'Enabled').subscribe();
+    const req = httpTesting.expectOne(
+      'api/rgw/bucket/foo?bucket_id=bar&uid=baz&versioning_state=Enabled'
+    );
     expect(req.request.method).toBe('PUT');
   });
 
index c60fcfba9f6f70fa8b6a43360fa126ddc94dbdd7..5e708c9fcfc7bfbe531cab3a32fa48c4520dc9ff 100644 (file)
@@ -58,10 +58,11 @@ export class RgwBucketService {
     return this.http.post(this.url, null, { params: params });
   }
 
-  update(bucket: string, bucketId: string, uid: string) {
+  update(bucket: string, bucketId: string, uid: string, versioningState: string) {
     let params = new HttpParams();
     params = params.append('bucket_id', bucketId);
     params = params.append('uid', uid);
+    params = params.append('versioning_state', versioningState);
     return this.http.put(`${this.url}/${bucket}`, null, { params: params });
   }
 
@@ -73,7 +74,7 @@ export class RgwBucketService {
 
   /**
    * Check if the specified bucket exists.
-   * @param {string} uid The bucket name to check.
+   * @param {string} bucket The bucket name to check.
    * @return {Observable<boolean>}
    */
   exists(bucket: string) {
index 116e530292ebec85b7726a82d0ada4b3dcb06967..fc841a1787c03b2e2bf1dd18ce4756608f896b43 100644 (file)
@@ -482,3 +482,34 @@ class RgwClient(RestClient):
             )
 
         return {'zonegroup': zonegroup_name, 'placement_targets': placement_targets}
+
+    @RestClient.api_get('/{bucket_name}?versioning')
+    def get_bucket_versioning(self, bucket_name, request=None):
+        """
+        Get bucket versioning.
+        :param str bucket_name: the name of the bucket.
+        :return: versioning state
+        :rtype: str
+        """
+        # pylint: disable=unused-argument
+        result = request()
+        if 'Status' not in result:
+            result['Status'] = 'Suspended'
+        return result['Status']
+
+    @RestClient.api_put('/{bucket_name}?versioning')
+    def set_bucket_versioning(self, bucket_name, versioning_state, request=None):
+        """
+        Set bucket versioning.
+        :param str bucket_name: the name of the bucket.
+        :param str versioning_state:
+            https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTVersioningStatus.html
+        :return: None
+        """
+        # pylint: disable=unused-argument
+        versioning_configuration = ET.Element('VersioningConfiguration')
+        status = ET.SubElement(versioning_configuration, 'Status')
+        status.text = versioning_state
+        data = ET.tostring(versioning_configuration, encoding='utf-8')
+
+        return request(data=data)