rgw_client = RgwClient.instance(owner, daemon_name)
return rgw_client.set_tags(bucket_name, tags)
+ def _get_lifecycle(self, bucket_name: str, daemon_name, owner):
+ rgw_client = RgwClient.instance(owner, daemon_name)
+ return rgw_client.get_lifecycle(bucket_name)
+
+ def _set_lifecycle(self, bucket_name: str, lifecycle: str, daemon_name, owner):
+ rgw_client = RgwClient.instance(owner, daemon_name)
+ return rgw_client.set_lifecycle(bucket_name, lifecycle)
+
+ def _delete_lifecycle(self, bucket_name: str, daemon_name, owner):
+ rgw_client = RgwClient.instance(owner, daemon_name)
+ return rgw_client.delete_lifecycle(bucket_name)
+
def _get_acl(self, bucket_name, daemon_name, owner):
rgw_client = RgwClient.instance(owner, daemon_name)
return str(rgw_client.get_acl(bucket_name))
result['bucket_policy'] = self._get_policy(bucket_name, daemon_name, result['owner'])
result['acl'] = self._get_acl(bucket_name, daemon_name, result['owner'])
result['replication'] = self._get_replication(bucket_name, result['owner'], daemon_name)
+ result['lifecycle'] = self._get_lifecycle(bucket_name, daemon_name, result['owner'])
# Append the locking configuration.
locking = self._get_locking(result['owner'], daemon_name, bucket_name)
mfa_delete=None, mfa_token_serial=None, mfa_token_pin=None,
lock_mode=None, lock_retention_period_days=None,
lock_retention_period_years=None, tags=None, bucket_policy=None,
- canned_acl=None, replication=None, daemon_name=None):
+ canned_acl=None, replication=None, lifecycle=None, daemon_name=None):
+ # pylint: disable=R0912
encryption_state = str_to_bool(encryption_state)
if replication is not None:
replication = str_to_bool(replication)
self._set_policy(bucket_name, bucket_policy, daemon_name, uid)
if canned_acl:
self._set_acl(bucket_name, canned_acl, uid, daemon_name)
- if replication is not None:
+ if replication:
self._set_replication(bucket_name, replication, uid, daemon_name)
+ if lifecycle and not lifecycle == '{}':
+ self._set_lifecycle(bucket_name, lifecycle, daemon_name, uid)
+ else:
+ self._delete_lifecycle(bucket_name, daemon_name, uid)
return self._append_bid(result)
def delete(self, bucket, purge_objects='true', daemon_name=None):
<a ngbNavLink
i18n>Policies</a>
<ng-template ngbNavContent>
-
- <table class="table table-striped table-bordered">
- <tbody>
- <tr>
- <td i18n
- class="bold w-25">Bucket policy</td>
- <td><pre>{{ selection.bucket_policy | json}}</pre></td>
- </tr>
- <tr>
- <td i18n
- class="bold w-25">Replication policy</td>
- <td><pre>{{ selection.replication | json}}</pre></td>
- </tr>
- <tr>
- <td i18n
- class="bold w-25">ACL</td>
- <td>
- <table class="table">
- <thead>
- <tr i18n>
- <th>Grantee</th>
- <th>Permissions</th>
- </tr>
- </thead>
- <tbody>
- <tr i18n>
- <td>Bucket Owner</td>
- <td>{{ aclPermissions.Owner || '-'}}</td>
- </tr>
- <tr i18n>
- <td>Everyone</td>
- <td>{{ aclPermissions.AllUsers || '-'}}</td>
- </tr>
- <tr i18n>
- <td>Authenticated users group</td>
- <td>{{ aclPermissions.AuthenticatedUsers || '-'}}</td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
+ <div class="table-scroller">
+ <table class="table table-striped table-bordered">
+ <tbody>
+ <tr>
+ <td i18n
+ class="bold w-25">Bucket policy</td>
+ <td><pre>{{ selection.bucket_policy | json}}</pre></td>
+ </tr>
+ <tr>
+ <div>
+ <td i18n
+ class="bold w-25">Lifecycle
+ <div *ngIf="(selection.lifecycle | json) !== '{}'"
+ class="input-group">
+ <button type="button"
+ class="btn btn-light"
+ [ngClass]="{'active': lifecycleFormat === 'json'}"
+ (click)="lifecycleFormat = 'json'">
+ JSON
+ </button>
+ <button type="button"
+ class="btn btn-light"
+ [ngClass]="{'active': lifecycleFormat === 'xml'}"
+ (click)="lifecycleFormat = 'xml'">
+ XML
+ </button>
+ </div>
+ </td>
+ </div>
+ <td>
+ <pre *ngIf="lifecycleFormat === 'json'">{{selection.lifecycle | json}}</pre>
+ <pre *ngIf="lifecycleFormat === 'xml'">{{ (selection.lifecycle | xml) || '-'}}</pre>
+ </td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold w-25">Replication policy</td>
+ <td><pre>{{ selection.replication | json}}</pre></td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold w-25">ACL</td>
+ <td>
+ <table class="table">
+ <thead>
+ <tr i18n>
+ <th>Grantee</th>
+ <th>Permissions</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr i18n>
+ <td>Bucket Owner</td>
+ <td>{{ aclPermissions.Owner || '-'}}</td>
+ </tr>
+ <tr i18n>
+ <td>Everyone</td>
+ <td>{{ aclPermissions.AllUsers || '-'}}</td>
+ </tr>
+ <tr i18n>
+ <td>Authenticated users group</td>
+ <td>{{ aclPermissions.AuthenticatedUsers || '-'}}</td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
</ng-template>
</ng-container>
</nav>
table td {
word-wrap: break-word;
}
+
+.table-scroller {
+ height: 100%;
+ max-height: 50vh;
+ overflow: auto;
+}
@Input()
selection: any;
+ lifecycleFormat: 'json' | 'xml' = 'json';
aclPermissions: Record<string, string[]> = {};
replicationStatus = $localize`Disabled`;
this.rgwBucketService.get(this.selection.bid).subscribe((bucket: object) => {
bucket['lock_retention_period_days'] = this.rgwBucketService.getLockDays(bucket);
this.selection = bucket;
+ if (this.lifecycleFormat === 'json' && !this.selection.lifecycle) {
+ this.selection.lifecycle = {};
+ }
this.aclPermissions = this.parseXmlAcl(this.selection.acl, this.selection.owner);
if (this.selection.replication?.['Rule']?.['Status']) {
this.replicationStatus = this.selection.replication?.['Rule']?.['Status'];
class="form-control resize-vertical"
id="bucket_policy"
formControlName="bucket_policy"
- (change)="bucketPolicyOnChange()">
+ (change)="textAreaOnChange('bucketPolicyTextArea')">
</textarea>
<span class="invalid-feedback"
*ngIf="bucketForm.showError('bucket_policy', frm, 'invalidJson')"
- i18n>Invalid json text</span>
+ i18n>Invalid json text.</span>
<button type="button"
id="clear-bucket-policy"
class="btn btn-light my-3"
- (click)="clearBucketPolicy()"
+ (click)="clearTextArea('bucket_policy', '{}')"
i18n>
<i [ngClass]="[icons.destroy]"></i>
Clear
</div>
</div>
+ <!-- Lifecycle -->
+ <div *ngIf="editing"
+ class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="id">Lifecycle
+ <cd-helper>JSON or XML formatted document</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <textarea #lifecycleTextArea
+ class="form-control resize-vertical"
+ id="lifecycle"
+ formControlName="lifecycle"
+ (change)="textAreaOnChange('lifecycleTextArea')">
+ </textarea>
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('lifecycle', frm, 'invalidJson')"
+ i18n>Invalid json text.</span>
+ <span class="invalid-feedback"
+ *ngIf="bucketForm.showError('lifecycle', frm, 'invalidXml')"
+ i18n>Invalid xml text.</span>
+ <button type="button"
+ id="clear-lifecycle"
+ class="btn btn-light my-3"
+ (click)="clearTextArea('lifecycle', '{}')"
+ i18n>
+ <i [ngClass]="[icons.destroy]"></i>
+ Clear
+ </button>
+ <div class="btn-group float-end"
+ role="group"
+ aria-label="bucket-policy-helpers">
+ <button type="button"
+ id="lifecycle-examples-button"
+ class="btn btn-light my-3"
+ (click)="openUrl('https://docs.aws.amazon.com/cli/latest/reference/s3api/put-bucket-lifecycle.html#examples')"
+ i18n>
+ <i [ngClass]="[icons.externalUrl]"></i>
+ Policy examples
+ </button>
+ </div>
+ </div>
+ </div>
+
<div class="form-group row">
<!-- ACL -->
import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
import { map, switchMap } from 'rxjs/operators';
+import { TextAreaXmlFormatterService } from '~/app/shared/services/text-area-xml-formatter.service';
@Component({
selector: 'cd-rgw-bucket-form',
export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewChecked {
@ViewChild('bucketPolicyTextArea')
public bucketPolicyTextArea: ElementRef<any>;
+ @ViewChild('lifecycleTextArea')
+ public lifecycleTextArea: ElementRef<any>;
bucketForm: CdFormGroup;
editing = false;
private notificationService: NotificationService,
private rgwEncryptionModal: RgwBucketEncryptionModel,
private textAreaJsonFormatterService: TextAreaJsonFormatterService,
+ private textAreaXmlFormatterService: TextAreaXmlFormatterService,
public actionLabels: ActionLabelsI18n,
private readonly changeDetectorRef: ChangeDetectorRef,
private rgwMultisiteService: RgwMultisiteService,
ngAfterViewChecked(): void {
this.changeDetectorRef.detectChanges();
- this.bucketPolicyOnChange();
+ this.textAreaOnChange(this.bucketPolicyTextArea);
+ this.textAreaOnChange(this.lifecycleTextArea);
}
createForm() {
lock_mode: ['COMPLIANCE'],
lock_retention_period_days: [10, [CdValidators.number(false), lockDaysValidator]],
bucket_policy: ['{}', CdValidators.json()],
+ lifecycle: ['{}', CdValidators.jsonOrXml()],
grantee: [Grantee.Owner, [Validators.required]],
aclPermission: [[aclPermission.FullControl], [Validators.required]],
replication: [false]
bidResp['acl'],
bidResp['owner']
);
+ value['lifecycle'] = JSON.stringify(bidResp['lifecycle'] || {});
}
this.bucketForm.setValue(value);
if (this.editing) {
xmlStrTags,
bucketPolicy,
cannedAcl,
- values['replication']
+ values['replication'],
+ values['lifecycle']
)
.subscribe(
() => {
});
}
- bucketPolicyOnChange() {
- if (this.bucketPolicyTextArea) {
- this.textAreaJsonFormatterService.format(this.bucketPolicyTextArea);
+ textAreaOnChange(textArea: ElementRef<any>) {
+ if (textArea?.nativeElement?.value?.startsWith?.('<')) {
+ this.textAreaXmlFormatterService.format(textArea);
+ } else {
+ this.textAreaJsonFormatterService.format(textArea);
}
}
window.open(url, '_blank');
}
- clearBucketPolicy() {
- this.bucketForm.get('bucket_policy').setValue('{}');
+ clearTextArea(field: string, defaultValue: string = '') {
+ this.bucketForm.get(field).setValue(defaultValue);
this.bucketForm.markAsDirty();
this.bucketForm.updateValueAndValidity();
}
null,
null,
'private',
- 'true'
+ 'true',
+ null
)
.subscribe();
const req = httpTesting.expectOne(
- `api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}&bucket_id=bar&uid=baz&versioning_state=Enabled&encryption_state=true&encryption_type=aws%253Akms&key_id=qwerty1&mfa_delete=Enabled&mfa_token_serial=1&mfa_token_pin=223344&lock_mode=GOVERNANCE&lock_retention_period_days=10&tags=null&bucket_policy=null&canned_acl=private&replication=true`
+ `api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}&bucket_id=bar&uid=baz&versioning_state=Enabled&encryption_state=true&encryption_type=aws%253Akms&key_id=qwerty1&mfa_delete=Enabled&mfa_token_serial=1&mfa_token_pin=223344&lock_mode=GOVERNANCE&lock_retention_period_days=10&tags=null&bucket_policy=null&canned_acl=private&replication=true&lifecycle=null`
);
expect(req.request.method).toBe('PUT');
});
tags: string,
bucketPolicy: string,
cannedAcl: string,
- replication: string
+ replication: string,
+ lifecycle: string
) {
return this.rgwDaemonService.request((params: HttpParams) => {
params = params.appendAll({
tags: tags,
bucket_policy: bucketPolicy,
canned_acl: cannedAcl,
- replication: replication
+ replication: replication,
+ lifecycle: lifecycle
});
return this.http.put(`${this.url}/${bucket}`, null, { params: params });
});
}
};
}
+
+ static xml(): ValidatorFn {
+ return (control: AbstractControl): Record<string, boolean> | null => {
+ if (!control.value) return null;
+ const parser = new DOMParser();
+ const xml = parser.parseFromString(control.value, 'application/xml');
+ const errorNode = xml.querySelector('parsererror');
+ if (errorNode) {
+ return { invalidXml: true };
+ }
+ return null;
+ };
+ }
+
+ static jsonOrXml(): ValidatorFn {
+ return (control: AbstractControl): Record<string, boolean> | null => {
+ if (!control.value) return null;
+
+ if (control.value.trim().startsWith('<')) {
+ const parser = new DOMParser();
+ const xml = parser.parseFromString(control.value, 'application/xml');
+ const errorNode = xml.querySelector('parsererror');
+ if (errorNode) {
+ return { invalidXml: true };
+ }
+ return null;
+ } else {
+ try {
+ JSON.parse(control.value);
+ return null;
+ } catch (e) {
+ return { invalidJson: true };
+ }
+ }
+ };
+ }
}
import { UpperFirstPipe } from './upper-first.pipe';
import { OctalToHumanReadablePipe } from './octal-to-human-readable.pipe';
import { PathPipe } from './path.pipe';
+import { XmlPipe } from './xml.pipe';
@NgModule({
imports: [CommonModule],
MdsSummaryPipe,
OsdSummaryPipe,
OctalToHumanReadablePipe,
- PathPipe
+ PathPipe,
+ XmlPipe
],
exports: [
ArrayPipe,
MdsSummaryPipe,
OsdSummaryPipe,
OctalToHumanReadablePipe,
- PathPipe
+ PathPipe,
+ XmlPipe
],
providers: [
ArrayPipe,
--- /dev/null
+import { TestBed } from '@angular/core/testing';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { JsonToXmlService } from '../services/json-to-xml.service';
+import { XmlPipe } from './xml.pipe';
+
+describe('XmlPipe', () => {
+ let pipe: XmlPipe;
+ let jsonToXmlService: JsonToXmlService;
+
+ configureTestBed({
+ providers: [JsonToXmlService]
+ });
+
+ beforeEach(() => {
+ jsonToXmlService = TestBed.inject(JsonToXmlService);
+ pipe = new XmlPipe(jsonToXmlService);
+ });
+
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+import { JsonToXmlService } from '../services/json-to-xml.service';
+
+@Pipe({
+ name: 'xml'
+})
+export class XmlPipe implements PipeTransform {
+ constructor(private jsonToXmlService: JsonToXmlService) {}
+
+ transform(value: string, valueFormat: string = 'json'): string {
+ if (valueFormat === 'json') {
+ value = this.jsonToXmlService.format(value);
+ }
+ return value;
+ }
+}
--- /dev/null
+import { TestBed } from '@angular/core/testing';
+
+import { JsonToXmlService } from './json-to-xml.service';
+
+describe('JsonToXmlService', () => {
+ let service: JsonToXmlService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(JsonToXmlService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should transform JSON formatted string to XML string', () => {
+ const json: string = `{
+ "foo": "bar",
+ "items": [
+ {
+ "name": "item1",
+ "value": "value1"
+ },
+ {
+ "name": "item2",
+ "value": "value2"
+ }
+ ]
+ }`;
+ const expectedXml = `<foo>bar</foo>
+<items>
+ <name>item1</name>
+ <value>value1</value>
+</items>
+<items>
+ <name>item2</name>
+ <value>value2</value>
+</items>
+`;
+ expect(JSON.parse(json)).toBeTruthy();
+ expect(service.format(json)).toBe(expectedXml);
+ });
+});
--- /dev/null
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class JsonToXmlService {
+ constructor() {}
+
+ format(json: any, indentSize: number = 2, currentIndent: number = 0): string {
+ if (!json) return null;
+ let xml = '';
+ if (typeof json === 'string') {
+ json = JSON.parse(json);
+ }
+
+ for (const key in json) {
+ if (json.hasOwnProperty(key)) {
+ const value = json[key];
+ const indentation = ' '.repeat(currentIndent);
+
+ if (Array.isArray(value)) {
+ value.forEach((item) => {
+ xml +=
+ `${indentation}<${key}>\n` +
+ this.format(item, indentSize, currentIndent + indentSize) +
+ `${indentation}</${key}>\n`;
+ });
+ } else if (typeof value === 'object') {
+ xml +=
+ `${indentation}<${key}>\n` +
+ this.format(value, indentSize, currentIndent + indentSize) +
+ `${indentation}</${key}>\n`;
+ } else {
+ xml += `${indentation}<${key}>${value}</${key}>\n`;
+ }
+ }
+ }
+ return xml;
+ }
+}
constructor() {}
format(textArea: ElementRef<any>): void {
- const value = textArea.nativeElement.value;
+ const value = textArea?.nativeElement?.value;
try {
const formatted = JSON.stringify(JSON.parse(value), null, 2);
textArea.nativeElement.value = formatted;
--- /dev/null
+import { TestBed } from '@angular/core/testing';
+
+import { TextAreaXmlFormatterService } from './text-area-xml-formatter.service';
+
+describe('TextAreaXmlFormatterService', () => {
+ let service: TextAreaXmlFormatterService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(TextAreaXmlFormatterService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
--- /dev/null
+import { ElementRef, Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class TextAreaXmlFormatterService {
+ constructor() {}
+
+ format(textArea: ElementRef<any>): void {
+ if (!textArea.nativeElement?.value) return;
+ const value = textArea.nativeElement.value;
+ const parser = new DOMParser();
+ const formatted = parser.parseFromString(value, 'application/xml');
+ const lineNumber = formatted.getElementsByTagName('*').length;
+ const pixelPerLine = 20;
+ const pixels = lineNumber * pixelPerLine;
+ textArea.nativeElement.style.height = pixels + 'px';
+ const errorNode = formatted.querySelector('parsererror');
+ if (errorNode) {
+ return;
+ }
+ }
+}
type: string
key_id:
type: string
+ lifecycle:
+ type: string
lock_mode:
type: string
lock_retention_period_days:
raise DashboardException(msg=str(e), component='rgw')
return result
+ @RestClient.api_get('/{bucket_name}?lifecycle')
+ def get_lifecycle(self, bucket_name, request=None):
+ # pylint: disable=unused-argument
+ try:
+ result = request() # type: ignore
+ result = {'LifecycleConfiguration': result}
+ except RequestException as e:
+ if e.content:
+ content = json_str_to_object(e.content)
+ if content.get(
+ 'Code') == 'NoSuchLifecycleConfiguration':
+ return None
+ raise DashboardException(msg=str(e), component='rgw')
+ return result
+
+ @staticmethod
+ def dict_to_xml(data):
+ if not data or data == '{}':
+ return ''
+ if isinstance(data, str):
+ try:
+ data = json.loads(data)
+ except json.JSONDecodeError:
+ raise DashboardException('Could not load json string')
+
+ def transform(data):
+ xml: str = ''
+ if isinstance(data, dict):
+ for key, value in data.items():
+ if isinstance(value, list):
+ for item in value:
+ if key == 'Rules':
+ key = 'Rule'
+ xml += f'<{key}>\n{transform(item)}</{key}>\n'
+ elif isinstance(value, dict):
+ xml += f'<{key}>\n{transform(value)}</{key}>\n'
+ else:
+ xml += f'<{key}>{str(value)}</{key}>\n'
+
+ elif isinstance(data, list):
+ for item in data:
+ xml += transform(item)
+ else:
+ xml += f'{data}'
+
+ return xml
+
+ return transform(data)
+
+ @RestClient.api_put('/{bucket_name}?lifecycle')
+ def set_lifecycle(self, bucket_name, lifecycle, request=None):
+ # pylint: disable=unused-argument
+ lifecycle = lifecycle.strip()
+ if lifecycle.startswith('{'):
+ lifecycle = RgwClient.dict_to_xml(lifecycle)
+ try:
+ if lifecycle and '<LifecycleConfiguration>' not in str(lifecycle):
+ lifecycle = f'<LifecycleConfiguration>{lifecycle}</LifecycleConfiguration>'
+ result = request(data=lifecycle) # type: ignore
+ except RequestException as e:
+ if e.content:
+ content = json_str_to_object(e.content)
+ if content.get("Code") == "MalformedXML":
+ msg = "Invalid Lifecycle document"
+ raise DashboardException(msg=msg, component='rgw')
+ raise DashboardException(msg=str(e), component='rgw')
+ return result
+
+ @RestClient.api_delete('/{bucket_name}?lifecycle')
+ def delete_lifecycle(self, bucket_name, request=None):
+ # pylint: disable=unused-argument
+ try:
+ result = request()
+ except RequestException as e:
+ raise DashboardException(msg=str(e), component='rgw')
+ return result
+
@RestClient.api_get('/{bucket_name}?object-lock')
def get_bucket_locking(self, bucket_name, request=None):
# type: (str, Optional[object]) -> dict
_parse_frontend_config('mongoose port=8080')
self.assertEqual(str(ctx.exception),
'Failed to determine RGW port from "mongoose port=8080"')
+
+
+class TestDictToXML(TestCase):
+ def test_empty_dict(self):
+ result = RgwClient.dict_to_xml({})
+ self.assertEqual(result, '')
+
+ def test_empty_string(self):
+ result = RgwClient.dict_to_xml("")
+ self.assertEqual(result, '')
+
+ def test_invalid_json_string(self):
+ with self.assertRaises(DashboardException):
+ RgwClient.dict_to_xml("invalid json")
+
+ def test_simple_dict(self):
+ data = {"name": "Foo", "age": 30}
+ expected_xml = "<name>Foo</name>\n<age>30</age>\n"
+ result = RgwClient.dict_to_xml(data)
+ self.assertEqual(result, expected_xml)
+
+ def test_nested_dict(self):
+ data = {"person": {"name": "Foo", "age": 30}}
+ expected_xml = "<person>\n<name>Foo</name>\n<age>30</age>\n</person>\n"
+ result = RgwClient.dict_to_xml(data)
+ self.assertEqual(result, expected_xml)
+
+ def test_list_in_dict(self):
+ data = {"names": ["Foo", "Boo"]}
+ expected_xml = "<names>\nFoo</names>\n<names>\nBoo</names>\n"
+ result = RgwClient.dict_to_xml(data)
+ self.assertEqual(result, expected_xml)
+
+ def test_rules_list_in_dict(self):
+ data = {"Rules": [{"id": 1}, {"id": 2}]}
+ expected_xml = "<Rule>\n<id>1</id>\n</Rule>\n<Rule>\n<id>2</id>\n</Rule>\n"
+ result = RgwClient.dict_to_xml(data)
+ self.assertEqual(result, expected_xml)
+
+ def test_json_string(self):
+ data = '{"name": "Foo", "age": 30}'
+ expected_xml = "<name>Foo</name>\n<age>30</age>\n"
+ result = RgwClient.dict_to_xml(data)
+ self.assertEqual(result, expected_xml)