# -*- coding: utf-8 -*-
from __future__ import absolute_import
+
import logging
+import urllib
+import six
-from .helper import DashboardTestCase
+from .helper import DashboardTestCase, JObj, JList, JLeaf
logger = logging.getLogger(__name__)
def setUpClass(cls):
cls.create_test_user = True
super(RgwBucketTest, cls).setUpClass()
+ # Create a tenanted user.
+ cls._radosgw_admin_cmd([
+ 'user', 'create', '--tenant', 'testx', '--uid', 'teuth-test-user',
+ '--display-name', 'tenanted teuth-test-user'
+ ])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls._radosgw_admin_cmd(['user', 'rm', '--tenant', 'testx', '--uid=teuth-test-user'])
+ super(RgwBucketTest, cls).tearDownClass()
def test_all(self):
# Create a new bucket.
})
self.assertStatus(201)
data = self.jsonBody()
- self.assertIn('bucket_info', data)
- data = data['bucket_info']
- self.assertIn('bucket', data)
- self.assertIn('quota', data)
- self.assertIn('creation_time', data)
- self.assertIn('name', data['bucket'])
- self.assertIn('bucket_id', data['bucket'])
- self.assertEqual(data['bucket']['name'], 'teuth-test-bucket')
+ self.assertSchema(data, JObj(sub_elems={
+ 'bucket_info': JObj(sub_elems={
+ 'bucket': JObj(allow_unknown=True, sub_elems={
+ 'name': JLeaf(str),
+ 'bucket_id': JLeaf(str),
+ 'tenant': JLeaf(str)
+ }),
+ 'quota': JObj(sub_elems={}, allow_unknown=True),
+ 'creation_time': JLeaf(str)
+ }, allow_unknown=True)
+ }, allow_unknown=True))
+ data = data['bucket_info']['bucket']
+ self.assertEqual(data['name'], 'teuth-test-bucket')
+ self.assertEqual(data['tenant'], '')
# List all buckets.
data = self._get('/api/rgw/bucket')
# Get the bucket.
data = self._get('/api/rgw/bucket/teuth-test-bucket')
self.assertStatus(200)
- self.assertIn('id', data)
- self.assertIn('bucket', data)
- self.assertIn('bucket_quota', data)
- self.assertIn('owner', data)
+ self.assertSchema(data, JObj(sub_elems={
+ 'id': JLeaf(str),
+ 'bid': JLeaf(str),
+ 'tenant': JLeaf(str),
+ 'bucket': JLeaf(str),
+ 'bucket_quota': JObj(sub_elems={}, allow_unknown=True),
+ 'owner': JLeaf(str)
+ }, allow_unknown=True))
self.assertEqual(data['bucket'], 'teuth-test-bucket')
self.assertEqual(data['owner'], 'admin')
self.assertStatus(200)
data = self._get('/api/rgw/bucket/teuth-test-bucket')
self.assertStatus(200)
+ self.assertSchema(data, JObj(sub_elems={
+ 'owner': JLeaf(str),
+ 'bid': JLeaf(str),
+ 'tenant': JLeaf(str)
+ }, allow_unknown=True))
self.assertEqual(data['owner'], 'teuth-test-user')
# Delete the bucket.
self.assertStatus(200)
self.assertEqual(len(data), 0)
+ def test_create_get_update_delete_w_tenant(self):
+ # Create a new bucket. The tenant of the user is used when
+ # the bucket is created.
+ self._post(
+ '/api/rgw/bucket',
+ params={
+ 'bucket': 'teuth-test-bucket',
+ 'uid': 'testx$teuth-test-user'
+ })
+ self.assertStatus(201)
+ # It's not possible to validate the result because there
+ # IS NO result object returned by the RGW Admin OPS API
+ # when a tenanted bucket is created.
+ data = self.jsonBody()
+ self.assertIsNone(data)
+
+ # List all buckets.
+ data = self._get('/api/rgw/bucket')
+ self.assertStatus(200)
+ self.assertEqual(len(data), 1)
+ self.assertIn('testx/teuth-test-bucket', data)
+
+ # Get the bucket.
+ data = self._get('/api/rgw/bucket/{}'.format(urllib.quote_plus(
+ 'testx/teuth-test-bucket')))
+ self.assertStatus(200)
+ self.assertSchema(data, JObj(sub_elems={
+ 'owner': JLeaf(str),
+ 'bucket': JLeaf(str),
+ 'tenant': JLeaf(str),
+ 'bid': JLeaf(str)
+ }, allow_unknown=True))
+ self.assertEqual(data['owner'], 'testx$teuth-test-user')
+ self.assertEqual(data['bucket'], 'teuth-test-bucket')
+ self.assertEqual(data['tenant'], 'testx')
+ self.assertEqual(data['bid'], 'testx/teuth-test-bucket')
+
+ # Update the bucket.
+ self._put(
+ '/api/rgw/bucket/{}'.format(urllib.quote_plus('testx/teuth-test-bucket')),
+ params={
+ 'bucket_id': data['id'],
+ 'uid': 'admin'
+ })
+ self.assertStatus(200)
+ data = self._get('/api/rgw/bucket/{}'.format(urllib.quote_plus(
+ 'testx/teuth-test-bucket')))
+ self.assertStatus(200)
+ self.assertIn('owner', data)
+ self.assertEqual(data['owner'], 'admin')
+
+ # Delete the bucket.
+ self._delete('/api/rgw/bucket/{}'.format(urllib.quote_plus(
+ 'testx/teuth-test-bucket')))
+ self.assertStatus(204)
+ data = self._get('/api/rgw/bucket')
+ self.assertStatus(200)
+ self.assertEqual(len(data), 0)
+
class RgwDaemonTest(DashboardTestCase):
super(RgwUserTest, cls).setUpClass()
def _assert_user_data(self, data):
- self.assertIn('caps', data)
- self.assertIn('display_name', data)
- self.assertIn('email', data)
- self.assertIn('keys', data)
+ self.assertSchema(data, JObj(sub_elems={
+ 'caps': JList(JObj(sub_elems={}, allow_unknown=True)),
+ 'display_name': JLeaf(str),
+ 'email': JLeaf(str),
+ 'keys': JList(JObj(sub_elems={}, allow_unknown=True)),
+ 'max_buckets': JLeaf(int),
+ 'subusers': JList(JLeaf(str)),
+ 'suspended': JLeaf(int),
+ 'swift_keys': JList(JObj(sub_elems={}, allow_unknown=True)),
+ 'tenant': JLeaf(str),
+ 'user_id': JLeaf(str),
+ 'uid': JLeaf(str)
+ }, allow_unknown=True))
self.assertGreaterEqual(len(data['keys']), 1)
- self.assertIn('max_buckets', data)
- self.assertIn('subusers', data)
- self.assertIn('suspended', data)
- self.assertIn('swift_keys', data)
- self.assertIn('tenant', data)
- self.assertIn('user_id', data)
def test_get(self):
data = self.get_rgw_user('admin')
self.assertGreaterEqual(len(data), 1)
self.assertIn('admin', data)
- def test_create_update_delete(self):
+ def test_create_get_update_delete(self):
# Create a new user.
self._post('/api/rgw/user', params={
'uid': 'teuth-test-user',
data = self.get_rgw_user('teuth-test-user')
self.assertStatus(200)
self._assert_user_data(data)
+ self.assertEqual(data['tenant'], '')
self.assertEqual(data['user_id'], 'teuth-test-user')
+ self.assertEqual(data['uid'], 'teuth-test-user')
# Update the user.
self._put(
self.assertIn('"HostId"', resp['detail'])
self.assertIn('"RequestId"', resp['detail'])
+ def test_create_get_update_delete_w_tenant(self):
+ # Create a new user.
+ self._post('/api/rgw/user', params={
+ 'uid': 'test01$teuth-test-user',
+ 'display_name': 'display name'
+ })
+ self.assertStatus(201)
+ data = self.jsonBody()
+ self._assert_user_data(data)
+ self.assertEqual(data['user_id'], 'teuth-test-user')
+ self.assertEqual(data['display_name'], 'display name')
+
+ # Get the user.
+ data = self.get_rgw_user('test01$teuth-test-user')
+ self.assertStatus(200)
+ self._assert_user_data(data)
+ self.assertEqual(data['tenant'], 'test01')
+ self.assertEqual(data['user_id'], 'teuth-test-user')
+ self.assertEqual(data['uid'], 'test01$teuth-test-user')
+
+ # Update the user.
+ self._put(
+ '/api/rgw/user/test01$teuth-test-user',
+ params={
+ 'display_name': 'new name'
+ })
+ self.assertStatus(200)
+ data = self.jsonBody()
+ self._assert_user_data(data)
+ self.assertEqual(data['display_name'], 'new name')
+
+ # Delete the user.
+ self._delete('/api/rgw/user/test01$teuth-test-user')
+ self.assertStatus(204)
+ self.get_rgw_user('test01$teuth-test-user')
+ self.assertStatus(500)
+ resp = self.jsonBody()
+ self.assertIn('detail', resp)
+ self.assertIn('failed request with status code 404', resp['detail'])
+ self.assertIn('"Code":"NoSuchUser"', resp['detail'])
+ self.assertIn('"HostId"', resp['detail'])
+ self.assertIn('"RequestId"', resp['detail'])
+
class RgwUserCapabilityTest(RgwTestCase):
@ApiController('/rgw/bucket', Scope.RGW)
class RgwBucket(RgwRESTController):
+ def _append_bid(self, bucket):
+ """
+ Append the bucket identifier that looks like [<tenant>/]<bucket>.
+ See http://docs.ceph.com/docs/nautilus/radosgw/multitenancy/ for
+ more information.
+ :param bucket: The bucket parameters.
+ :type bucket: dict
+ :return: The modified bucket parameters including the 'bid' parameter.
+ :rtype: dict
+ """
+ if isinstance(bucket, dict):
+ bucket['bid'] = '{}/{}'.format(bucket['tenant'], bucket['bucket']) \
+ if bucket['tenant'] else bucket['bucket']
+ return bucket
+
def list(self):
return self.proxy('GET', 'bucket')
def get(self, bucket):
- return self.proxy('GET', 'bucket', {'bucket': bucket})
+ result = self.proxy('GET', 'bucket', {'bucket': bucket})
+ return self._append_bid(result)
def create(self, bucket, uid):
try:
raise DashboardException(e, http_status_code=500, component='rgw')
def set(self, bucket, bucket_id, uid):
- return self.proxy('PUT', 'bucket', {
+ result = self.proxy('PUT', 'bucket', {
'bucket': bucket,
'bucket-id': bucket_id,
'uid': uid
}, json_response=False)
+ return self._append_bid(result)
def delete(self, bucket, purge_objects='true'):
return self.proxy('DELETE', 'bucket', {
@ApiController('/rgw/user', Scope.RGW)
class RgwUser(RgwRESTController):
+ def _append_uid(self, user):
+ """
+ Append the user identifier that looks like [<tenant>$]<user>.
+ See http://docs.ceph.com/docs/jewel/radosgw/multitenancy/ for
+ more information.
+ :param user: The user parameters.
+ :type user: dict
+ :return: The modified user parameters including the 'uid' parameter.
+ :rtype: dict
+ """
+ if isinstance(user, dict):
+ user['uid'] = '{}${}'.format(user['tenant'], user['user_id']) \
+ if user['tenant'] else user['user_id']
+ return user
+
def list(self):
return self.proxy('GET', 'metadata/user')
def get(self, uid):
- return self.proxy('GET', 'user', {'uid': uid})
+ result = self.proxy('GET', 'user', {'uid': uid})
+ return self._append_uid(result)
def create(self, uid, display_name, email=None, max_buckets=None,
suspended=None, generate_key=None, access_key=None,
params['access-key'] = access_key
if secret_key is not None:
params['secret-key'] = secret_key
- return self.proxy('PUT', 'user', params)
+ result = self.proxy('PUT', 'user', params)
+ return self._append_uid(result)
def set(self, uid, display_name=None, email=None, max_buckets=None,
suspended=None):
params['max-buckets'] = max_buckets
if suspended is not None:
params['suspended'] = suspended
- return self.proxy('POST', 'user', params)
+ result = self.proxy('POST', 'user', params)
+ return self._append_uid(result)
def delete(self, uid):
try:
children: [
{ path: '', component: RgwBucketListComponent },
{ path: 'add', component: RgwBucketFormComponent, data: { breadcrumbs: 'Add' } },
- { path: 'edit/:bucket', component: RgwBucketFormComponent, data: { breadcrumbs: 'Edit' } }
+ { path: 'edit/:bid', component: RgwBucketFormComponent, data: { breadcrumbs: 'Edit' } }
]
}
]
<tr>
<td i18n
class="bold col-sm-1">Name</td>
- <td class="col-sm-3">{{ bucket.bucket }}</td>
+ <td class="col-sm-3">{{ bucket.bid }}</td>
</tr>
<tr>
<td i18n
<!-- Name -->
<div class="form-group"
- [ngClass]="{'has-error': bucketForm.showError('bucket', frm)}">
+ [ngClass]="{'has-error': bucketForm.showError('bid', frm)}">
<label class="control-label col-sm-3"
- for="bucket">
+ for="bid">
<ng-container i18n>Name</ng-container>
<span class="required"
*ngIf="!editing"></span>
</label>
<div class="col-sm-9">
- <input id="bucket"
- name="bucket"
+ <input id="bid"
+ name="bid"
class="form-control"
type="text"
i18n-placeholder
placeholder="Name..."
- formControlName="bucket"
+ formControlName="bid"
[readonly]="editing"
autofocus>
<span class="help-block"
- *ngIf="bucketForm.showError('bucket', frm, 'required')"
+ *ngIf="bucketForm.showError('bid', frm, 'required')"
i18n>This field is required.</span>
<span class="help-block"
- *ngIf="bucketForm.showError('bucket', frm, 'bucketNameInvalid')"
+ *ngIf="bucketForm.showError('bid', frm, 'bucketNameInvalid')"
i18n>The value is not valid.</span>
<span class="help-block"
- *ngIf="bucketForm.showError('bucket', frm, 'bucketNameExists')"
+ *ngIf="bucketForm.showError('bid', frm, 'bucketNameExists')"
i18n>The chosen name is already in use.</span>
</div>
</div>
createForm() {
this.bucketForm = this.formBuilder.group({
id: [null],
- bucket: [null, [Validators.required], [this.bucketNameValidator()]],
+ bid: [null, [Validators.required], [this.bucketNameValidator()]],
owner: [null, [Validators.required]]
});
}
// Process route parameters.
this.route.params.subscribe(
- (params: { bucket: string }) => {
- if (!params.hasOwnProperty('bucket')) {
+ (params: { bid: string }) => {
+ if (!params.hasOwnProperty('bid')) {
return;
}
- params.bucket = decodeURIComponent(params.bucket);
+ const bid = decodeURIComponent(params.bid);
this.loading = true;
// Load the bucket data in 'edit' mode.
this.editing = true;
- this.rgwBucketService.get(params.bucket).subscribe((resp: object) => {
+ this.rgwBucketService.get(bid).subscribe((resp: object) => {
this.loading = false;
// Get the default values.
const defaults = _.clone(this.bucketForm.value);
if (this.bucketForm.pristine) {
this.goToListView();
}
- const bucketCtl = this.bucketForm.get('bucket');
+ const bidCtl = this.bucketForm.get('bid');
const ownerCtl = this.bucketForm.get('owner');
if (this.editing) {
// Edit
const idCtl = this.bucketForm.get('id');
- this.rgwBucketService.update(bucketCtl.value, idCtl.value, ownerCtl.value).subscribe(
+ this.rgwBucketService.update(bidCtl.value, idCtl.value, ownerCtl.value).subscribe(
() => {
this.goToListView();
},
);
} else {
// Add
- this.rgwBucketService.create(bucketCtl.value, ownerCtl.value).subscribe(
+ this.rgwBucketService.create(bidCtl.value, ownerCtl.value).subscribe(
() => {
this.goToListView();
},
this.columns = [
{
name: this.i18n('Name'),
- prop: 'bucket',
+ prop: 'bid',
flexGrow: 1
},
{
}
];
const getBucketUri = () =>
- this.selection.first() && `${encodeURI(this.selection.first().bucket)}`;
+ this.selection.first() && `${encodeURIComponent(this.selection.first().bid)}`;
const addAction: CdTableAction = {
permission: 'create',
icon: 'fa-plus',
// Delete all selected data table rows.
observableForkJoin(
this.selection.selected.map((bucket: any) => {
- return this.rgwBucketService.delete(bucket.bucket);
+ return this.rgwBucketService.delete(bucket.bid);
})
).subscribe(
null,
<tr>
<td i18n
class="bold col-sm-1">Username</td>
- <td class="col-sm-3">{{ user.user_id }}</td>
+ <td class="col-sm-3">{{ user.uid }}</td>
</tr>
<tr>
<td i18n
class="bold col-sm-1">Suspended</td>
<td class="col-sm-3">{{ user.suspended ? "Yes" : "No" }}</td>
</tr>
+ <tr>
+ <td i18n
+ class="bold col-sm-1">System</td>
+ <td class="col-sm-3">{{ user.system ? "Yes" : "No" }}</td>
+ </tr>
<tr>
<td i18n
class="bold col-sm-1">Maximum buckets</td>
this.user.caps = _.sortBy(this.user.caps, 'type');
// Load the user/bucket quota of the selected user.
- if (this.user.tenant === '') {
- this.rgwUserService.getQuota(this.user.user_id).subscribe((resp: object) => {
- _.extend(this.user, resp);
- });
- }
+ this.rgwUserService.getQuota(this.user.uid).subscribe((resp: object) => {
+ _.extend(this.user, resp);
+ });
// Process the keys.
this.keys = [];
<!-- Username -->
<div class="form-group"
- [ngClass]="{'has-error': userForm.showError('user_id', frm)}">
+ [ngClass]="{'has-error': userForm.showError('uid', frm)}">
<label class="control-label col-sm-3"
- for="user_id">
+ for="uid">
<ng-container i18n>Username</ng-container>
<span class="required"
*ngIf="!editing">
</span>
</label>
<div class="col-sm-9">
- <input id="user_id"
+ <input id="uid"
class="form-control"
type="text"
- formControlName="user_id"
+ formControlName="uid"
[readonly]="editing"
autofocus>
<span class="help-block"
- *ngIf="userForm.showError('user_id', frm, 'required')"
+ *ngIf="userForm.showError('uid', frm, 'required')"
i18n>This field is required.</span>
<span class="help-block"
- *ngIf="userForm.showError('user_id', frm, 'notUnique')"
+ *ngIf="userForm.showError('uid', frm, 'notUnique')"
i18n>The chosen user ID is already in use.</span>
</div>
</div>
});
it('should validate that username is required', () => {
- formHelper.expectErrorChange('user_id', '', 'required', true);
+ formHelper.expectErrorChange('uid', '', 'required', true);
});
it('should validate that username is valid', fakeAsync(() => {
- formHelper.setValue('user_id', 'ab', true);
+ formHelper.setValue('uid', 'ab', true);
tick(500);
- formHelper.expectValid('user_id');
+ formHelper.expectValid('uid');
}));
it('should validate that username is invalid', fakeAsync(() => {
- formHelper.setValue('user_id', 'abc', true);
+ formHelper.setValue('uid', 'abc', true);
tick(500);
- formHelper.expectError('user_id', 'notUnique');
+ formHelper.expectError('uid', 'notUnique');
}));
});
createForm() {
this.userForm = this.formBuilder.group({
// General
- user_id: [
+ uid: [
null,
[Validators.required],
[CdValidators.unique(this.rgwUserService.exists, this.rgwUserService)]
if (!params.hasOwnProperty('uid')) {
return;
}
+ const uid = decodeURIComponent(params.uid);
this.loading = true;
// Load the user data in 'edit' mode.
this.editing = true;
// Load the user and quota information.
const observables = [];
- observables.push(this.rgwUserService.get(params.uid));
- observables.push(this.rgwUserService.getQuota(params.uid));
+ observables.push(this.rgwUserService.get(uid));
+ observables.push(this.rgwUserService.getQuota(uid));
observableForkJoin(observables).subscribe(
(resp: any[]) => {
this.loading = false;
if (this.userForm.pristine) {
this.goToListView();
}
- const uid = this.userForm.getValue('user_id');
+ const uid = this.userForm.getValue('uid');
if (this.editing) {
// Edit
if (this._isGeneralDirty()) {
'full-control': 'full',
'read-write': 'readwrite'
};
- const uid = this.userForm.getValue('user_id');
+ const uid = this.userForm.getValue('uid');
const args = {
subuser: subuser.id,
access:
const subuser = this.subusers[index];
// Create an observable to delete the subuser when the form is submitted.
this.submitObservables.push(
- this.rgwUserService.deleteSubuser(this.userForm.getValue('user_id'), subuser.id)
+ this.rgwUserService.deleteSubuser(this.userForm.getValue('uid'), subuser.id)
);
// Remove the associated S3 keys.
this.s3Keys = this.s3Keys.filter((key) => {
* Add/Update a capability.
*/
setCapability(cap: RgwUserCapability, index?: number) {
- const uid = this.userForm.getValue('user_id');
+ const uid = this.userForm.getValue('uid');
if (_.isNumber(index)) {
// Modify
const oldCap = this.capabilities[index];
const cap = this.capabilities[index];
// Create an observable to delete the capability when the form is submitted.
this.submitObservables.push(
- this.rgwUserService.deleteCapability(this.userForm.getValue('user_id'), cap.type, cap.perm)
+ this.rgwUserService.deleteCapability(this.userForm.getValue('uid'), cap.type, cap.perm)
);
// Remove the capability to update the UI.
this.capabilities.splice(index, 1);
const key = this.s3Keys[index];
// Create an observable to delete the S3 key when the form is submitted.
this.submitObservables.push(
- this.rgwUserService.deleteS3Key(this.userForm.getValue('user_id'), key.access_key)
+ this.rgwUserService.deleteS3Key(this.userForm.getValue('uid'), key.access_key)
);
// Remove the S3 key to update the UI.
this.s3Keys.splice(index, 1);
* @param {number | undefined} index The subuser to show.
*/
showSubuserModal(index?: number) {
- const uid = this.userForm.getValue('user_id');
+ const uid = this.userForm.getValue('uid');
const modalRef = this.bsModalService.show(RgwUserSubuserModalComponent);
if (_.isNumber(index)) {
// Edit
*/
private _getCreateArgs() {
const result = {
- uid: this.userForm.getValue('user_id'),
+ uid: this.userForm.getValue('uid'),
display_name: this.userForm.getValue('display_name'),
suspended: this.userForm.getValue('suspended'),
email: '',
private _getS3KeyUserCandidates() {
let result = [];
// Add the current user id.
- const user_id = this.userForm.getValue('user_id');
- if (_.isString(user_id) && !_.isEmpty(user_id)) {
- result.push(user_id);
+ const uid = this.userForm.getValue('uid');
+ if (_.isString(uid) && !_.isEmpty(uid)) {
+ result.push(uid);
}
// Append the subusers.
this.subusers.forEach((subUser) => {
this.columns = [
{
name: this.i18n('Username'),
- prop: 'user_id',
+ prop: 'uid',
flexGrow: 1
},
{
flexGrow: 1
}
];
- const getUserUri = () => this.selection.first() && this.selection.first().user_id;
+ const getUserUri = () =>
+ this.selection.first() && `${encodeURIComponent(this.selection.first().uid)}`;
const addAction: CdTableAction = {
permission: 'create',
icon: 'fa-plus',
// Delete all selected data table rows.
observableForkJoin(
this.selection.selected.map((user: any) => {
- return this.rgwUserService.delete(user.user_id);
+ return this.rgwUserService.delete(user.uid);
})
).subscribe(
null,