From 9d1700cbafd761e7b16a0f6687f01025edd2fa6e Mon Sep 17 00:00:00 2001 From: alfonsomthd Date: Fri, 19 Jul 2019 16:02:44 +0200 Subject: [PATCH] mgr/dashboard: select placement target on RGW bucket creation * Select a placement target from the zone that the RGW daemon is running on. Fixes: https://tracker.ceph.com/issues/40567 Signed-off-by: alfonsomthd --- qa/tasks/mgr/dashboard/test_rgw.py | 8 +- .../mgr/dashboard/controllers/health.py | 25 +++-- src/pybind/mgr/dashboard/controllers/rgw.py | 24 ++++- .../frontend/e2e/rgw/buckets.e2e-spec.ts | 28 +++--- .../dashboard/frontend/e2e/rgw/buckets.po.ts | 27 +++++- .../rgw-bucket-form.component.html | 40 +++++++- .../rgw-bucket-form.component.spec.ts | 42 +++++++-- .../rgw-bucket-form.component.ts | 55 ++++++++--- .../app/shared/api/rgw-bucket.service.spec.ts | 6 +- .../src/app/shared/api/rgw-bucket.service.ts | 5 +- .../app/shared/api/rgw-site.service.spec.ts | 34 +++++++ .../src/app/shared/api/rgw-site.service.ts | 22 +++++ .../mgr/dashboard/services/rgw_client.py | 55 ++++++++++- .../mgr/dashboard/tests/test_rgw_client.py | 92 +++++++++++++++++++ src/pybind/mgr/dashboard/tests/test_tools.py | 18 +++- src/pybind/mgr/dashboard/tools.py | 35 +++++++ 16 files changed, 451 insertions(+), 65 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-site.service.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-site.service.ts diff --git a/qa/tasks/mgr/dashboard/test_rgw.py b/qa/tasks/mgr/dashboard/test_rgw.py index bbb9df987122f..77f6c47985a1a 100644 --- a/qa/tasks/mgr/dashboard/test_rgw.py +++ b/qa/tasks/mgr/dashboard/test_rgw.py @@ -131,7 +131,9 @@ class RgwBucketTest(RgwTestCase): '/api/rgw/bucket', params={ 'bucket': 'teuth-test-bucket', - 'uid': 'admin' + 'uid': 'admin', + 'zonegroup': 'default', + 'placement_target': 'default-placement' }) self.assertStatus(201) data = self.jsonBody() @@ -201,7 +203,9 @@ class RgwBucketTest(RgwTestCase): '/api/rgw/bucket', params={ 'bucket': 'teuth-test-bucket', - 'uid': 'testx$teuth-test-user' + 'uid': 'testx$teuth-test-user', + 'zonegroup': 'default', + 'placement_target': 'default-placement' }) self.assertStatus(201) # It's not possible to validate the result because there diff --git a/src/pybind/mgr/dashboard/controllers/health.py b/src/pybind/mgr/dashboard/controllers/health.py index 30d1e5285299a..ecb771cd01f36 100644 --- a/src/pybind/mgr/dashboard/controllers/health.py +++ b/src/pybind/mgr/dashboard/controllers/health.py @@ -11,6 +11,7 @@ from ..security import Permission, Scope from ..services.ceph_service import CephService from ..services.iscsi_cli import IscsiGatewaysConfig from ..services.iscsi_client import IscsiClient +from ..tools import partial_dict class HealthData(object): @@ -26,10 +27,6 @@ class HealthData(object): self._has_permissions = auth_callback self._minimal = minimal - @staticmethod - def _partial_dict(orig, keys): - return {k: orig[k] for k in keys} - def all_health(self): result = { "health": self.basic_health(), @@ -83,7 +80,7 @@ class HealthData(object): def client_perf(self): result = CephService.get_client_perf() if self._minimal: - result = self._partial_dict( + result = partial_dict( result, ['read_bytes_sec', 'read_op_per_sec', 'recovering_bytes_per_sec', 'write_bytes_sec', @@ -97,7 +94,7 @@ class HealthData(object): del df['stats_by_class'] if self._minimal: - df = dict(stats=self._partial_dict( + df = dict(stats=partial_dict( df['stats'], ['total_avail_bytes', 'total_bytes', 'total_used_raw_bytes'] @@ -107,15 +104,15 @@ class HealthData(object): def fs_map(self): fs_map = mgr.get('fs_map') if self._minimal: - fs_map = self._partial_dict(fs_map, ['filesystems', 'standbys']) + fs_map = partial_dict(fs_map, ['filesystems', 'standbys']) fs_map['standbys'] = [{}] * len(fs_map['standbys']) - fs_map['filesystems'] = [self._partial_dict(item, ['mdsmap']) for + fs_map['filesystems'] = [partial_dict(item, ['mdsmap']) for item in fs_map['filesystems']] for fs in fs_map['filesystems']: mdsmap_info = fs['mdsmap']['info'] min_mdsmap_info = dict() for k, v in mdsmap_info.items(): - min_mdsmap_info[k] = self._partial_dict(v, ['state']) + min_mdsmap_info[k] = partial_dict(v, ['state']) fs['mdsmap'] = dict(info=min_mdsmap_info) return fs_map @@ -136,15 +133,15 @@ class HealthData(object): def mgr_map(self): mgr_map = mgr.get('mgr_map') if self._minimal: - mgr_map = self._partial_dict(mgr_map, ['active_name', 'standbys']) + mgr_map = partial_dict(mgr_map, ['active_name', 'standbys']) mgr_map['standbys'] = [{}] * len(mgr_map['standbys']) return mgr_map def mon_status(self): mon_status = json.loads(mgr.get('mon_status')['json']) if self._minimal: - mon_status = self._partial_dict(mon_status, ['monmap', 'quorum']) - mon_status['monmap'] = self._partial_dict( + mon_status = partial_dict(mon_status, ['monmap', 'quorum']) + mon_status['monmap'] = partial_dict( mon_status['monmap'], ['mons'] ) mon_status['monmap']['mons'] = [{}] * \ @@ -157,9 +154,9 @@ class HealthData(object): # Not needed, skip the effort of transmitting this to UI del osd_map['pg_temp'] if self._minimal: - osd_map = self._partial_dict(osd_map, ['osds']) + osd_map = partial_dict(osd_map, ['osds']) osd_map['osds'] = [ - self._partial_dict(item, ['in', 'up']) + partial_dict(item, ['in', 'up']) for item in osd_map['osds'] ] else: diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index 871fa00fdf4ee..485159c646d09 100644 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -13,6 +13,7 @@ from ..rest_client import RequestException from ..security import Scope from ..services.ceph_service import CephService from ..services.rgw_client import RgwClient +from ..tools import json_str_to_object @ApiController('/rgw', Scope.RGW) @@ -96,13 +97,28 @@ class RgwRESTController(RESTController): try: instance = RgwClient.admin_instance() result = instance.proxy(method, path, params, None) - if json_response and result != '': - result = json.loads(result.decode('utf-8')) + if json_response: + result = json_str_to_object(result) return result except (DashboardException, RequestException) as e: raise DashboardException(e, http_status_code=500, component='rgw') +@ApiController('/rgw/site', Scope.RGW) +class RgwSite(RgwRESTController): + + def list(self, query=None): + if query == 'placement-targets': + instance = RgwClient.admin_instance() + result = instance.get_placement_targets() + else: + # @TODO: (it'll be required for multisite workflows): + # by default, retrieve cluster realms/zonegroups map. + raise DashboardException(http_status_code=501, component='rgw', msg='Not Implemented') + + return result + + @ApiController('/rgw/bucket', Scope.RGW) class RgwBucket(RgwRESTController): @@ -128,10 +144,10 @@ class RgwBucket(RgwRESTController): result = self.proxy('GET', 'bucket', {'bucket': bucket}) return self._append_bid(result) - def create(self, bucket, uid): + def create(self, bucket, uid, zonegroup, placement_target): try: rgw_client = RgwClient.instance(uid) - return rgw_client.create_bucket(bucket) + return rgw_client.create_bucket(bucket, zonegroup, placement_target) except RequestException as e: raise DashboardException(e, http_status_code=500, component='rgw') diff --git a/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.e2e-spec.ts index d6685f4541c71..0f5e6b707f38f 100644 --- a/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.e2e-spec.ts @@ -1,11 +1,13 @@ import { Helper } from '../helper.po'; import { PageHelper } from '../page-helper.po'; +import { BucketsPageHelper } from './buckets.po'; describe('RGW buckets page', () => { - let buckets; + let buckets: BucketsPageHelper; beforeAll(() => { buckets = new Helper().buckets; + buckets.navigateTo(); }); afterEach(() => { @@ -13,22 +15,18 @@ describe('RGW buckets page', () => { }); describe('breadcrumb test', () => { - beforeAll(() => { - buckets.navigateTo(); - }); - it('should open and show breadcrumb', () => { expect(PageHelper.getBreadcrumbText()).toEqual('Buckets'); }); }); describe('create, edit & delete bucket test', () => { - beforeAll(() => { - buckets.navigateTo(); - }); - it('should create bucket', () => { - buckets.create('000test', '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'); + buckets.create( + '000test', + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + 'default-placement' + ); expect(PageHelper.getTableCell('000test').isPresent()).toBe(true); }); @@ -44,16 +42,16 @@ describe('RGW buckets page', () => { }); describe('Invalid Input in Create and Edit tests', () => { - beforeAll(() => { - buckets.navigateTo(); - }); - it('should test invalid inputs in create fields', () => { buckets.invalidCreate(); }); it('should test invalid input in edit owner field', () => { - buckets.create('000rq', '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'); + buckets.create( + '000rq', + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + 'default-placement' + ); buckets.invalidEdit('000rq'); buckets.delete('000rq'); }); diff --git a/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.po.ts index 3506a019075af..834c501f792e9 100644 --- a/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.po.ts +++ b/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.po.ts @@ -8,7 +8,7 @@ export class BucketsPageHelper extends PageHelper { create: '/#/rgw/bucket/create' }; - create(name, owner) { + create(name, owner, placementTarget) { this.navigateTo('create'); // Enter in bucket name @@ -19,6 +19,11 @@ export class BucketsPageHelper extends PageHelper { element(by.cssContainingText('select[name=owner] option', owner)).click(); expect(element(by.id('owner')).getAttribute('class')).toContain('ng-valid'); + // Select bucket placement target: + element(by.id('owner')).click(); + element(by.cssContainingText('select[name=placement-target] option', placementTarget)).click(); + expect(element(by.id('placement-target')).getAttribute('class')).toContain('ng-valid'); + // Click the create button and wait for bucket to be made const createButton = element(by.cssContainingText('button', 'Create Bucket')); createButton.click().then(() => { @@ -39,6 +44,10 @@ export class BucketsPageHelper extends PageHelper { expect(PageHelper.getBreadcrumbText()).toEqual('Edit'); + expect(element(by.css('input[name=placement-target]')).getAttribute('value')).toBe( + 'default-placement' + ); + const ownerDropDown = element(by.id('owner')); ownerDropDown.click(); // click owner dropdown menu @@ -134,6 +143,22 @@ export class BucketsPageHelper extends PageHelper { 'This field is required.' ); + // Check invalid placement target input + PageHelper.moveClick(ownerDropDown); + element(by.cssContainingText('select[name=owner] option', 'dev')).click(); + // The drop down error message will not appear unless a valid option is previsously selected. + element( + by.cssContainingText('select[name=placement-target] option', 'default-placement') + ).click(); + element( + by.cssContainingText('select[name=placement-target] option', 'Select a placement target') + ).click(); + PageHelper.moveClick(nameInputField); // To trigger a validation + expect(element(by.id('placement-target')).getAttribute('class')).toContain('ng-invalid'); + expect(element(by.css('#placement-target + .invalid-feedback')).getText()).toMatch( + 'This field is required.' + ); + // Clicks the Create Bucket button but the page doesn't move. Done by testing // for the breadcrumb PageHelper.moveClick(element(by.cssContainingText('button', 'Create Bucket'))); // Clicks Create Bucket button diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html index 9eb6e67fed0d3..43d0f467986f8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html @@ -19,7 +19,7 @@
+ +
+ +
+ + + This field is required. + + + + +
+
+