* 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 <almartin@redhat.com>
'/api/rgw/bucket',
params={
'bucket': 'teuth-test-bucket',
- 'uid': 'admin'
+ 'uid': 'admin',
+ 'zonegroup': 'default',
+ 'placement_target': 'default-placement'
})
self.assertStatus(201)
data = self.jsonBody()
'/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
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):
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(),
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',
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']
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
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'] = [{}] * \
# 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:
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)
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):
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')
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(() => {
});
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);
});
});
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');
});
create: '/#/rgw/bucket/create'
};
- create(name, owner) {
+ create(name, owner, placementTarget) {
this.navigateTo('create');
// Enter in bucket name
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(() => {
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
'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
<div class="form-group row"
*ngIf="editing">
<label i18n
- class="col-sm-3 col-form-label"
+ class="col-form-label col-sm-3"
for="id">Id</label>
<div class="col-sm-9">
<input id="id"
</div>
</div>
+ <!-- Placement target -->
+ <div class="form-group row">
+ <label class="col-form-label col-sm-3"
+ for="placement-target">
+ <ng-container i18n>Placement target</ng-container>
+ <span class="required"
+ *ngIf="!editing"></span>
+ </label>
+ <div class="col-sm-9">
+ <ng-template #placementTargetSelect>
+ <select id="placement-target"
+ name="placement-target"
+ formControlName="placement-target"
+ class="form-control custom-select">
+ <option i18n
+ *ngIf="placementTargets === null"
+ [ngValue]="null">Loading...</option>
+ <option i18n
+ *ngIf="placementTargets !== null"
+ [ngValue]="null">-- Select a placement target --</option>
+ <option *ngFor="let placementTarget of placementTargets"
+ [value]="placementTarget.name">{{ placementTarget.description }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('placement-target', frm, 'required')"
+ i18n>This field is required.</span>
+ </ng-template>
+ <ng-container *ngIf="editing; else placementTargetSelect">
+ <input id="placement-target"
+ name="placement-target"
+ formControlName="placement-target"
+ class="form-control"
+ type="text"
+ readonly>
+ </ng-container>
+ </div>
+ </div>
+
</div>
<div class="card-footer">
<div class="button-group text-right">
import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
import { RgwBucketService } from '../../../shared/api/rgw-bucket.service';
+import { RgwSiteService } from '../../../shared/api/rgw-site.service';
import { NotificationType } from '../../../shared/enum/notification-type.enum';
import { NotificationService } from '../../../shared/services/notification.service';
import { SharedModule } from '../../../shared/shared.module';
describe('RgwBucketFormComponent', () => {
let component: RgwBucketFormComponent;
let fixture: ComponentFixture<RgwBucketFormComponent>;
- let rwgBucketService: RgwBucketService;
+ let rgwBucketService: RgwBucketService;
+ let getPlacementTargetsSpy;
configureTestBed({
declarations: [RgwBucketFormComponent],
beforeEach(() => {
fixture = TestBed.createComponent(RgwBucketFormComponent);
component = fixture.componentInstance;
- fixture.detectChanges();
- rwgBucketService = TestBed.get(RgwBucketService);
+ rgwBucketService = TestBed.get(RgwBucketService);
+ getPlacementTargetsSpy = spyOn(TestBed.get(RgwSiteService), 'getPlacementTargets');
});
it('should create', () => {
});
it('should validate name (4/4)', () => {
- spyOn(rwgBucketService, 'enumerate').and.returnValue(observableOf(['abcd']));
+ spyOn(rgwBucketService, 'enumerate').and.returnValue(observableOf(['abcd']));
const validatorFn = component.bucketNameValidator();
const ctrl = new FormControl('abcd');
ctrl.markAsDirty();
});
}
});
+
+ it('should get zonegroup and placement targets', () => {
+ const payload = {
+ zonegroup: 'default',
+ placement_targets: [
+ {
+ name: 'default-placement',
+ data_pool: 'default.rgw.buckets.data'
+ },
+ {
+ name: 'placement-target2',
+ data_pool: 'placement-target2.rgw.buckets.data'
+ }
+ ]
+ };
+ getPlacementTargetsSpy.and.returnValue(observableOf(payload));
+ fixture.detectChanges();
+
+ expect(component.zonegroup).toBe(payload.zonegroup);
+ const placementTargets = [];
+ for (const placementTarget of payload['placement_targets']) {
+ placementTarget['description'] = `${placementTarget['name']} (pool: ${
+ placementTarget['data_pool']
+ })`;
+ placementTargets.push(placementTarget);
+ }
+ expect(component.placementTargets).toEqual(placementTargets);
+ });
});
describe('submit form', () => {
});
it('tests create success notification', () => {
- spyOn(rwgBucketService, 'create').and.returnValue(observableOf([]));
+ spyOn(rgwBucketService, 'create').and.returnValue(observableOf([]));
component.editing = false;
component.bucketForm.markAsDirty();
component.submit();
});
it('tests update success notification', () => {
- spyOn(rwgBucketService, 'update').and.returnValue(observableOf([]));
+ spyOn(rgwBucketService, 'update').and.returnValue(observableOf([]));
component.editing = true;
component.bucketForm.markAsDirty();
component.submit();
import * as _ from 'lodash';
import { RgwBucketService } from '../../../shared/api/rgw-bucket.service';
+import { RgwSiteService } from '../../../shared/api/rgw-site.service';
import { RgwUserService } from '../../../shared/api/rgw-user.service';
import { ActionLabelsI18n, URLVerbs } from '../../../shared/constants/app.constants';
import { NotificationType } from '../../../shared/enum/notification-type.enum';
owners = null;
action: string;
resource: string;
+ zonegroup: string;
+ placementTargets: Object[] = [];
constructor(
private route: ActivatedRoute,
private router: Router,
private formBuilder: CdFormBuilder,
private rgwBucketService: RgwBucketService,
+ private rgwSiteService: RgwSiteService,
private rgwUserService: RgwUserService,
private notificationService: NotificationService,
private i18n: I18n,
this.bucketForm = this.formBuilder.group({
id: [null],
bid: [null, [Validators.required], [this.bucketNameValidator()]],
- owner: [null, [Validators.required]]
+ owner: [null, [Validators.required]],
+ 'placement-target': [null, this.editing ? [] : [Validators.required]]
});
}
this.owners = resp.sort();
});
+ if (!this.editing) {
+ // Get placement targets:
+ this.rgwSiteService.getPlacementTargets().subscribe((placementTargets) => {
+ this.zonegroup = placementTargets['zonegroup'];
+ _.forEach(placementTargets['placement_targets'], (placementTarget) => {
+ placementTarget['description'] = `${placementTarget['name']} (${this.i18n('pool')}: ${
+ placementTarget['data_pool']
+ })`;
+ this.placementTargets.push(placementTarget);
+ });
+
+ // If there is only 1 placement target, select it by default:
+ if (this.placementTargets.length === 1) {
+ this.bucketForm.get('placement-target').setValue(this.placementTargets[0]['name']);
+ }
+ });
+ }
+
// Process route parameters.
this.route.params.subscribe(
(params: { bid: string }) => {
const defaults = _.clone(this.bucketForm.value);
// Extract the values displayed in the form.
let value = _.pick(resp, _.keys(this.bucketForm.value));
+ value['placement-target'] = resp['placement_rule'];
// Append default values.
value = _.merge(defaults, value);
// Update the form.
}
const bidCtl = this.bucketForm.get('bid');
const ownerCtl = this.bucketForm.get('owner');
+ const placementTargetCtl = this.bucketForm.get('placement-target');
if (this.editing) {
// Edit
const idCtl = this.bucketForm.get('id');
);
} else {
// Add
- this.rgwBucketService.create(bidCtl.value, ownerCtl.value).subscribe(
- () => {
- this.notificationService.show(
- NotificationType.success,
- this.i18n('Created Object Gateway bucket "{{bid}}"', { bid: bidCtl.value })
- );
- this.goToListView();
- },
- () => {
- // Reset the 'Submit' button.
- this.bucketForm.setErrors({ cdSubmitButton: true });
- }
- );
+ this.rgwBucketService
+ .create(bidCtl.value, ownerCtl.value, this.zonegroup, placementTargetCtl.value)
+ .subscribe(
+ () => {
+ this.notificationService.show(
+ NotificationType.success,
+ this.i18n('Created Object Gateway bucket "{{bid}}"', { bid: bidCtl.value })
+ );
+ this.goToListView();
+ },
+ () => {
+ // Reset the 'Submit' button.
+ this.bucketForm.setErrors({ cdSubmitButton: true });
+ }
+ );
}
}
});
it('should call create', () => {
- service.create('foo', 'bar').subscribe();
- const req = httpTesting.expectOne('api/rgw/bucket?bucket=foo&uid=bar');
+ service.create('foo', 'bar', 'default', 'default-placement').subscribe();
+ const req = httpTesting.expectOne(
+ 'api/rgw/bucket?bucket=foo&uid=bar&zonegroup=default&placement_target=default-placement'
+ );
expect(req.request.method).toBe('POST');
});
return this.http.get(`${this.url}/${bucket}`);
}
- create(bucket: string, uid: string) {
+ create(bucket: string, uid: string, zonegroup: string, placementTarget: string) {
let params = new HttpParams();
params = params.append('bucket', bucket);
params = params.append('uid', uid);
+ params = params.append('zonegroup', zonegroup);
+ params = params.append('placement_target', placementTarget);
+
return this.http.post(this.url, null, { params: params });
}
--- /dev/null
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '../../../testing/unit-test-helper';
+import { RgwSiteService } from './rgw-site.service';
+
+describe('RgwSiteService', () => {
+ let service: RgwSiteService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [RgwSiteService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.get(RgwSiteService);
+ httpTesting = TestBed.get(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should call getPlacementTargets', () => {
+ service.getPlacementTargets().subscribe();
+ const req = httpTesting.expectOne('api/rgw/site?query=placement-targets');
+ expect(req.request.method).toBe('GET');
+ });
+});
--- /dev/null
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { cdEncode } from '../decorators/cd-encode';
+import { ApiModule } from './api.module';
+
+@cdEncode
+@Injectable({
+ providedIn: ApiModule
+})
+export class RgwSiteService {
+ private url = 'api/rgw/site';
+
+ constructor(private http: HttpClient) {}
+
+ getPlacementTargets() {
+ let params = new HttpParams();
+ params = params.append('query', 'placement-targets');
+
+ return this.http.get(this.url, { params: params });
+ }
+}
import re
import ipaddress
from distutils.util import strtobool
+import xml.etree.ElementTree as ET
import six
from ..awsauth import S3Auth
from ..settings import Settings, Options
from ..rest_client import RestClient, RequestException
-from ..tools import build_url, dict_contains_path
+from ..tools import build_url, dict_contains_path, json_str_to_object, partial_dict
from .. import mgr, logger
+try:
+ from typing import Any, Dict, List # pylint: disable=unused-import
+except ImportError:
+ pass # For typing only
+
class NoCredentialsException(RequestException):
def __init__(self):
# Append the instance to the internal map.
RgwClient._user_instances[RgwClient._SYSTEM_USERID] = instance
+ def _get_daemon_zone_info(self): # type: () -> Dict[str, Any]
+ return json_str_to_object(self.proxy('GET', 'config?type=zone', None, None))
+
+ def _get_daemon_zonegroup_map(self): # type: () -> List[Dict[str, Any]]
+ zonegroups = json_str_to_object(
+ self.proxy('GET', 'config?type=zonegroup-map', None, None)
+ )
+
+ return [partial_dict(
+ zonegroup['val'],
+ ['api_name', 'zones']
+ ) for zonegroup in zonegroups['zonegroups']]
+
@staticmethod
def _rgw_settings():
return (Settings.RGW_API_HOST,
raise e
@RestClient.api_put('/{bucket_name}')
- def create_bucket(self, bucket_name, request=None):
- logger.info("Creating bucket: %s", bucket_name)
- return request()
+ def create_bucket(self, bucket_name, zonegroup, placement_target, request=None):
+ logger.info("Creating bucket: %s, zonegroup: %s, placement_target: %s",
+ bucket_name, zonegroup, placement_target)
+ create_bucket_configuration = ET.Element('CreateBucketConfiguration')
+ location_constraint = ET.SubElement(create_bucket_configuration, 'LocationConstraint')
+ location_constraint.text = '{}:{}'.format(zonegroup, placement_target)
+
+ return request(data=ET.tostring(create_bucket_configuration, encoding='utf-8'))
+
+ def get_placement_targets(self): # type: () -> Dict[str, Any]
+ zone = self._get_daemon_zone_info()
+ # A zone without realm id can only belong to default zonegroup.
+ zonegroup_name = 'default'
+ if zone['realm_id']:
+ zonegroup_map = self._get_daemon_zonegroup_map()
+ for zonegroup in zonegroup_map:
+ for realm_zone in zonegroup['zones']:
+ if realm_zone['id'] == zone['id']:
+ zonegroup_name = zonegroup['api_name']
+ break
+
+ placement_targets = [] # type: List[Dict]
+ for placement_pool in zone['placement_pools']:
+ placement_targets.append(
+ {
+ 'name': placement_pool['key'],
+ 'data_pool': placement_pool['val']['storage_classes']['STANDARD']['data_pool']
+ }
+ )
+
+ return {'zonegroup': zonegroup_name, 'placement_targets': placement_targets}
# -*- coding: utf-8 -*-
import unittest
+from mock import patch
from .. import mgr
from ..services.rgw_client import RgwClient
mgr.set_module_option('RGW_API_SSL_VERIFY', False)
instance = RgwClient.admin_instance()
self.assertFalse(instance.session.verify)
+
+ @patch.object(RgwClient, '_get_daemon_zone_info')
+ def test_get_placement_targets_from_default_zone(self, zone_info):
+ zone_info.return_value = {
+ 'placement_pools': [
+ {
+ 'key': 'default-placement',
+ 'val': {
+ 'index_pool': 'default.rgw.buckets.index',
+ 'storage_classes': {
+ 'STANDARD': {
+ 'data_pool': 'default.rgw.buckets.data'
+ }
+ },
+ 'data_extra_pool': 'default.rgw.buckets.non-ec',
+ 'index_type': 0
+ }
+ }
+ ],
+ 'realm_id': ''
+ }
+
+ instance = RgwClient.admin_instance()
+ expected_result = {
+ 'zonegroup': 'default',
+ 'placement_targets': [
+ {
+ 'name': 'default-placement',
+ 'data_pool': 'default.rgw.buckets.data'
+ }
+ ]
+ }
+ self.assertEqual(expected_result, instance.get_placement_targets())
+
+ @patch.object(RgwClient, '_get_daemon_zone_info')
+ @patch.object(RgwClient, '_get_daemon_zonegroup_map')
+ def test_get_placement_targets_from_realm_zone(self, zonegroup_map, zone_info):
+ zone_info.return_value = {
+ 'id': 'a0df30ea-4b5b-4830-b143-2bedf684663d',
+ 'placement_pools': [
+ {
+ 'key': 'default-placement',
+ 'val': {
+ 'index_pool': 'default.rgw.buckets.index',
+ 'storage_classes': {
+ 'STANDARD': {
+ 'data_pool': 'default.rgw.buckets.data'
+ }
+ }
+ }
+ }
+ ],
+ 'realm_id': 'b5a25d1b-e7ed-4fe5-b461-74f24b8e759b'
+ }
+
+ zonegroup_map.return_value = [
+ {
+ 'api_name': 'zonegroup1-realm1',
+ 'zones': [
+ {
+ 'id': '2ef7d0ef-7616-4e9c-8553-b732ebf0592b'
+ },
+ {
+ 'id': 'b1d15925-6c8e-408e-8485-5a62cbccfe1f'
+ }
+ ]
+ },
+ {
+ 'api_name': 'zonegroup2-realm1',
+ 'zones': [
+ {
+ 'id': '645f0f59-8fcc-4e11-95d5-24f289ee8e25'
+ },
+ {
+ 'id': 'a0df30ea-4b5b-4830-b143-2bedf684663d'
+ }
+ ]
+ }
+ ]
+
+ instance = RgwClient.admin_instance()
+ expected_result = {
+ 'zonegroup': 'zonegroup2-realm1',
+ 'placement_targets': [
+ {
+ 'name': 'default-placement',
+ 'data_pool': 'default.rgw.buckets.data'
+ }
+ ]
+ }
+ self.assertEqual(expected_result, instance.get_placement_targets())
from ..services.exception import handle_rados_error
from ..controllers import RESTController, ApiController, Controller, \
BaseController, Proxy
-from ..tools import dict_contains_path, RequestLoggingTool
+from ..tools import dict_contains_path, json_str_to_object, partial_dict, RequestLoggingTool
# pylint: disable=W0613
self.assertTrue(dict_contains_path(x, ['a']))
self.assertFalse(dict_contains_path(x, ['a', 'c']))
self.assertTrue(dict_contains_path(x, []))
+
+ def test_json_str_to_object(self):
+ expected_result = {'a': 1, 'b': 'bbb'}
+ self.assertEqual(expected_result, json_str_to_object('{"a": 1, "b": "bbb"}'))
+ self.assertEqual(expected_result, json_str_to_object(b'{"a": 1, "b": "bbb"}'))
+ self.assertEqual('', json_str_to_object(''))
+ self.assertRaises(TypeError, json_str_to_object, None)
+
+ def test_partial_dict(self):
+ expected_result = {'a': 1, 'c': 3}
+ self.assertEqual(expected_result, partial_dict({'a': 1, 'b': 2, 'c': 3}, ['a', 'c']))
+ self.assertEqual({}, partial_dict({'a': 1, 'b': 2, 'c': 3}, []))
+ self.assertEqual({}, partial_dict({}, []))
+ self.assertRaises(KeyError, partial_dict, {'a': 1, 'b': 2, 'c': 3}, ['d'])
+ self.assertRaises(TypeError, partial_dict, None, ['a'])
+ self.assertRaises(TypeError, partial_dict, {'a': 1, 'b': 2, 'c': 3}, None)
from .settings import Settings
from .services.auth import JwtManager
+try:
+ from typing import Any, AnyStr, Dict, List # pylint: disable=unused-import
+except ImportError:
+ pass # For typing only
+
class RequestLoggingTool(cherrypy.Tool):
def __init__(self):
return bool(strtobool(val))
+def json_str_to_object(value): # type: (AnyStr) -> Any
+ """
+ It converts a JSON valid string representation to object.
+
+ >>> result = json_str_to_object('{"a": 1}')
+ >>> result == {'a': 1}
+ True
+ """
+ if value == '':
+ return value
+
+ try:
+ # json.loads accepts binary input from version >=3.6
+ value = value.decode('utf-8')
+ except AttributeError:
+ pass
+
+ return json.loads(value)
+
+
+def partial_dict(orig, keys): # type: (Dict, List[str]) -> Dict
+ """
+ It returns Dict containing only the selected keys of original Dict.
+
+ >>> partial_dict({'a': 1, 'b': 2}, ['b'])
+ {'b': 2}
+ """
+ return {k: orig[k] for k in keys}
+
+
def get_request_body_params(request):
"""
Helper function to get parameters from the request body.