]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: RGW - Create Storage Class 61606/head
authorDnyaneshwari <dnyaneshwari@li-9c9fbecc-2d5c-11b2-a85c-e2a7cc8a424f.ibm.com>
Fri, 31 Jan 2025 09:35:12 +0000 (15:05 +0530)
committerDnyaneshwari <dnyaneshwari@li-9c9fbecc-2d5c-11b2-a85c-e2a7cc8a424f.ibm.com>
Mon, 17 Feb 2025 13:06:07 +0000 (18:36 +0530)
Fixes: https://tracker.ceph.com/issues/69750
Signed-off-by: Dnyaneshwari Talwekar <dtalweka@redhat.com>
16 files changed:
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-storage-class.model.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-storage-class.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-storage-class.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/directives/directives.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/directives/optional-field.directive.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/directives/optional-field.directive.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rgw_client.py
src/pybind/mgr/dashboard/services/service.py

index b2e225a37b0429f5c544161b1b53ed40012d1658..ef8903afb082ec3081e2f9f7f720cf913f55c8a0 100755 (executable)
@@ -1189,6 +1189,14 @@ class RgwZonegroup(RESTController):
         result = multisite_instance.delete_placement_targets(placement_id, storage_class)
         return result
 
+    @Endpoint('POST', path='storage-class')
+    @CreatePermission
+    # pylint: disable=W0102
+    def storage_class(self, zone_group, placement_targets: List[Dict[str, str]] = []):
+        multisite_instance = RgwMultisite()
+        result = multisite_instance.add_placement_targets(zone_group, placement_targets)
+        return result
+
     @Endpoint()
     @ReadPermission
     def get_all_zonegroups_info(self):
index 2fd9ede9ec03f0e4836f55d05820aade7757de19..0e671db749944c9aa870068382adc007ee93c513 100644 (file)
@@ -1,5 +1,6 @@
 export interface ZoneGroupDetails {
   default_zonegroup: string;
+  name: string;
   zonegroups: ZoneGroup[];
 }
 
@@ -18,10 +19,6 @@ export interface StorageClassDetails {
   multipart_sync_threshold: number;
   host_style: string;
 }
-export interface ZoneGroup {
-  name: string;
-  placement_targets: Target[];
-}
 
 export interface S3Details {
   endpoint: string;
@@ -49,4 +46,55 @@ export interface Target {
   tier_targets: TierTarget[];
 }
 
+export interface StorageClassDetails {
+  target_path: string;
+  access_key: string;
+  secret: string;
+  multipart_min_part_size: number;
+  multipart_sync_threshold: number;
+  host_style: string;
+}
+export interface ZoneGroup {
+  name: string;
+  id: string;
+  placement_targets?: Target[];
+}
+
+export interface S3Details {
+  endpoint: string;
+  access_key: string;
+  storage_class: string;
+  target_path: string;
+  target_storage_class: string;
+  region: string;
+  secret: string;
+  multipart_min_part_size: number;
+  multipart_sync_threshold: number;
+  host_style: boolean;
+}
+
+export interface RequestModel {
+  zone_group: string;
+  placement_targets: PlacementTarget[];
+}
+
+export interface PlacementTarget {
+  tags: string[];
+  placement_id: string;
+  storage_class: string;
+  tier_type: typeof CLOUD_TIER;
+  tier_config: {
+    endpoint: string;
+    access_key: string;
+    secret: string;
+    target_path: string;
+    retain_head_object: boolean;
+    region: string;
+    multipart_sync_threshold: number;
+    multipart_min_part_size: number;
+  };
+}
+
 export const CLOUD_TIER = 'cloud-s3';
+
+export const DEFAULT_PLACEMENT = 'default-placement';
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.html
new file mode 100644 (file)
index 0000000..6d38088
--- /dev/null
@@ -0,0 +1,356 @@
+<div cdsCol
+     [columnNumbers]="{ md: 4 }">
+  <form name="storageClassForm"
+        #formDir="ngForm"
+        [formGroup]="storageClassForm"
+        novalidate>
+    <div i18n="form title"
+         class="form-header">
+      {{ action | titlecase }} {{ resource | upperFirst }}
+    </div>
+    <legend>
+      <cd-help-text i18n>
+        All fields are required, except where marked optional.
+      </cd-help-text>
+    </legend>
+    <div class="form-item form-item-append"
+         cdsRow>
+      <div cdsCol>
+        <!-- Zone Group -->
+        <cds-select
+          label="Zone Group Name"
+          i18n-label
+          formControlName="zonegroup"
+          id="zonegroup"
+          [invalid]="
+            storageClassForm.controls.zonegroup.invalid && storageClassForm.controls.zonegroup.dirty
+          "
+          (change)="onZonegroupChange()"
+          [invalidText]="zonegroupError"
+        >
+          <option *ngFor="let zonegrp of zonegroupNames"
+                  [value]="zonegrp.name"
+                  [selected]="zonegrp.name === storageClassForm.getValue('zonegroup')"
+                  i18n>
+            {{ zonegrp.name }}
+          </option>
+        </cds-select>
+        <ng-template #zonegroupError>
+          <span
+            class="invalid-feedback"
+            *ngIf="storageClassForm.showError('zonegroup', formDir, 'required')"
+            i18n
+            >This field is required.</span
+          >
+        </ng-template>
+      </div>
+      <div cdsCol>
+        <!-- Placement Target -->
+        <cds-select
+          label="Placement Target"
+          i18n-label
+          formControlName="placement_target"
+          id="placement_target"
+          [invalid]="
+            storageClassForm.controls.placement_target.invalid &&
+            storageClassForm.controls.placement_target.dirty
+          "
+          [invalidText]="placementError"
+        >
+        <option [value]=""
+                i18n> --Select-- </option>
+        <option *ngFor="let placementTarget of placementTargets"
+                [value]="placementTarget"
+                [selected]="placementTarget === storageClassForm.getValue('placement_target')"
+                i18n>
+            {{ placementTarget }}
+        </option>
+        </cds-select>
+        <ng-template #placementError>
+          <span
+            class="invalid-feedback"
+            *ngIf="storageClassForm.showError('placement_target', formDir, 'required')"
+            i18n
+            >This field is required.</span
+          >
+        </ng-template>
+      </div>
+    </div>
+    <!-- Storage Class -->
+    <div class="form-item">
+      <cds-text-label
+        labelInputID="storage_class"
+        i18n
+        [invalid]="
+          storageClassForm.controls.storage_class.invalid &&
+          storageClassForm.controls.storage_class.dirty
+        "
+        [invalidText]="storageError"
+        >Storage Class Name
+        <input
+          cdsText
+          type="type"
+          id="storage_class"
+          formControlName="storage_class"
+          [invalid]="
+            storageClassForm.controls.storage_class.invalid &&
+            storageClassForm.controls.storage_class.dirty
+          "
+        />
+      </cds-text-label>
+      <ng-template #storageError>
+        <span
+          class="invalid-feedback"
+          *ngIf="storageClassForm.showError('storage_class', formDir, 'required')"
+          i18n
+          >This field is required.</span
+        >
+      </ng-template>
+    </div>
+    <div class="form-item form-item-append"
+         cdsRow>
+      <div cdsCol>
+        <!-- Target Region -->
+        <cds-text-label
+          labelInputID="region"
+          i18n
+          [invalid]="
+            storageClassForm.controls.region.invalid && storageClassForm.controls.region.dirty
+          "
+          [invalidText]="regionError"
+          [helperText]="targetRegionText"
+          >Target Region
+          <input
+            cdsText
+            type="text"
+            id="region"
+            formControlName="region"
+            placeholder="e.g, us-east-1"
+            i18n-placeholder
+            [invalid]="
+              storageClassForm.controls.region.invalid && storageClassForm.controls.region.dirty
+            "
+          />
+        </cds-text-label>
+        <ng-template #regionError>
+          <span
+            class="invalid-feedback"
+            *ngIf="storageClassForm.showError('region', formDir, 'required')"
+            i18n
+            >This field is required.</span
+          >
+        </ng-template>
+      </div>
+      <div cdsCol>
+        <!-- Target Endpoint -->
+        <cds-text-label
+          labelInputID="endpoint"
+          i18n
+          [invalid]="
+            storageClassForm.controls.endpoint.invalid && storageClassForm.controls.endpoint.dirty
+          "
+          [invalidText]="endpointError"
+          [helperText]="targetEndpointText"
+          >Target Endpoint
+          <input
+            cdsText
+            type="text"
+            placeholder="e.g, http://ceph-node-00.com:80"
+            i18n-placeholder
+            id="endpoint"
+            formControlName="endpoint"
+            [invalid]="
+              storageClassForm.controls.endpoint.invalid && storageClassForm.controls.endpoint.dirty
+            "
+          />
+        </cds-text-label>
+        <ng-template #endpointError>
+          <span
+            class="invalid-feedback"
+            *ngIf="storageClassForm.showError('endpoint', formDir, 'required')"
+            i18n
+            >This field is required.</span
+          >
+        </ng-template>
+      </div>
+    </div>
+
+    <!-- Access Key  -->
+    <div class="form-item">
+      <div cdsCol
+           [columnNumbers]="{ md: 12 }"
+           class="d-flex">
+        <cds-password-label
+          labelInputID="access_key"
+          [invalid]="
+            !storageClassForm.controls.access_key.valid &&
+            storageClassForm.controls.access_key.dirty
+          "
+          [invalidText]="accessError"
+          [helperText]="targetAccessKeyText"
+          i18n
+          >Target Access Key
+          <input
+            cdsPassword
+            type="password"
+            id="access_key"
+            formControlName="access_key"
+            [invalid]="
+              !storageClassForm.controls.access_key.valid &&
+              storageClassForm.controls.access_key.dirty
+            "
+          />
+        </cds-password-label>
+        <cd-copy-2-clipboard-button class="clipboard"> </cd-copy-2-clipboard-button>
+        <ng-template #accessError>
+          <span
+            class="invalid-feedback"
+            *ngIf="storageClassForm.showError('access_key', formDir, 'required')"
+            i18n
+            >This field is required.</span
+          >
+        </ng-template>
+      </div>
+    </div>
+
+    <!-- Secret Key  -->
+    <div class="form-item">
+      <div cdsCol
+           [columnNumbers]="{ md: 12 }"
+           class="d-flex">
+        <cds-password-label
+          labelInputID="secret_key"
+          [helperText]="targetSecretKeyText"
+          [invalid]="
+            !storageClassForm.controls.secret_key.valid &&
+            storageClassForm.controls.secret_key.dirty
+          "
+          [invalidText]="secretError"
+          i18n
+          >Target Secret Key
+          <input
+            cdsPassword
+            type="password"
+            id="secret_key"
+            formControlName="secret_key"
+            [invalid]="
+              !storageClassForm.controls.secret_key.valid &&
+              storageClassForm.controls.secret_key.dirty
+            "
+          />
+        </cds-password-label>
+        <cd-copy-2-clipboard-button class="clipboard"> </cd-copy-2-clipboard-button>
+        <ng-template #secretError>
+          <span
+            class="invalid-feedback"
+            *ngIf="storageClassForm.showError('secret_key', formDir, 'required')"
+            i18n
+            >This field is required.</span
+          >
+        </ng-template>
+      </div>
+    </div>
+
+    <!-- Target Path -->
+    <div class="form-item">
+      <cds-text-label
+        labelInputID="target_path"
+        i18n
+        [invalid]="
+          storageClassForm.controls.target_path.invalid &&
+          storageClassForm.controls.target_path.dirty
+        "
+        [invalidText]="targetError"
+        [helperText]="targetPathText"
+        >Target Path
+        <input
+          cdsText
+          type="text"
+          id="target_path"
+          formControlName="target_path"
+          [invalid]="
+            storageClassForm.controls.target_path.invalid &&
+            storageClassForm.controls.target_path.dirty
+          "
+        />
+      </cds-text-label>
+      <ng-template #targetError>
+        <span
+          class="invalid-feedback"
+          *ngIf="storageClassForm.showError('target_path', formDir, 'required')"
+          i18n
+          >This field is required.</span
+        >
+      </ng-template>
+    </div>
+
+    <fieldset>
+      <cds-accordion size="lg"
+                     class="form-item">
+        <cds-accordion-item
+          [title]="title"
+          id="advanced-fieldset"
+          (selected)="showAdvanced = !showAdvanced"
+        >
+          <!-- Multi Part Sync Threshold -->
+          <div class="form-item form-item-append"
+               cdsRow>
+            <div cdsCol>
+              <cds-text-label
+                labelInputID="multipart_sync_threshold"
+                i18n
+                [helperText]="multipartSyncThreholdText"
+                cdOptionalField="Multipart Sync Threshold"
+                >Multipart Sync Threshold
+                <input
+                  cdsText
+                  type="text"
+                  id="multipart_sync_threshold"
+                  formControlName="multipart_sync_threshold"
+                />
+              </cds-text-label>
+            </div>
+            <div cdsCol>
+              <cds-text-label
+                labelInputID="multipart_min_part_size"
+                i18n
+                [helperText]="multipartMinPartText"
+                cdOptionalField="Multipart Minimum Part Size"
+                >Multipart Minimum Part Size
+                <input
+                  cdsText
+                  type="text"
+                  id="multipart_min_part_size"
+                  formControlName="multipart_min_part_size"
+                />
+              </cds-text-label>
+            </div>
+          </div>
+          <div class="form-item">
+            <cds-checkbox
+              id="retain_head_object"
+              formControlName="retain_head_object"
+              cdOptionalField="Retain Head Object"
+              i18n-label
+              >Retain Head Object
+              <cd-help-text>{{ retainHeadObjectText }}</cd-help-text>
+            </cds-checkbox>
+          </div>
+        </cds-accordion-item>
+      </cds-accordion>
+      <ng-template #title>
+        <h5 class="cds--accordion__title cd-header">Advanced</h5>
+      </ng-template>
+    </fieldset>
+    <cd-alert-panel type="warning"
+                    spacingClass="mb-2">
+      <span i18n>RGW service would be restarted after creating the storage class.</span>
+    </cd-alert-panel>
+    <cd-form-button-panel
+      (submitActionEvent)="submitAction()"
+      [form]="storageClassForm"
+      [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+      wrappingClass="text-right"
+    ></cd-form-button-panel>
+  </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.scss
new file mode 100644 (file)
index 0000000..f8e869b
--- /dev/null
@@ -0,0 +1,5 @@
+@use '@carbon/layout';
+
+.clipboard {
+  margin-top: layout.$spacing-06;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.spec.ts
new file mode 100644 (file)
index 0000000..2f1c43c
--- /dev/null
@@ -0,0 +1,140 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { SharedModule } from '~/app/shared/shared.module';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { ToastrModule } from 'ngx-toastr';
+import {
+  CheckboxModule,
+  ComboBoxModule,
+  GridModule,
+  InputModule,
+  SelectModule
+} from 'carbon-components-angular';
+import { CoreModule } from '~/app/core/core.module';
+import { RgwStorageClassFormComponent } from './rgw-storage-class-form.component';
+
+describe('RgwStorageClassFormComponent', () => {
+  let component: RgwStorageClassFormComponent;
+  let fixture: ComponentFixture<RgwStorageClassFormComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [
+        BrowserAnimationsModule,
+        SharedModule,
+        HttpClientTestingModule,
+        RouterTestingModule,
+        ReactiveFormsModule,
+        ToastrModule.forRoot(),
+        GridModule,
+        InputModule,
+        CoreModule,
+        SelectModule,
+        ComboBoxModule,
+        CheckboxModule
+      ],
+      declarations: [RgwStorageClassFormComponent]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(RgwStorageClassFormComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    component.goToListView();
+    expect(component).toBeTruthy();
+  });
+
+  it('should initialize the form with empty values', () => {
+    const storageClassForm = component.storageClassForm;
+    expect(storageClassForm).toBeTruthy();
+    expect(storageClassForm.get('zonegroup')).toBeTruthy();
+    expect(storageClassForm.get('placement_target')).toBeTruthy();
+  });
+
+  it('on zonegroup changes', () => {
+    component.zoneGroupDeatils = {
+      default_zonegroup: 'zonegroup1',
+      name: 'zonegrp1',
+      zonegroups: [
+        {
+          name: 'zonegroup1',
+          id: 'zonegroup-id-1',
+          placement_targets: [
+            {
+              name: 'default-placement',
+              tier_targets: [
+                {
+                  val: {
+                    storage_class: 'CLOUDIBM',
+                    tier_type: 'cloud-s3',
+                    s3: {
+                      endpoint: 'https://s3.amazonaws.com',
+                      access_key: 'ACCESSKEY',
+                      storage_class: 'STANDARD',
+                      target_path: '/path/to/storage',
+                      target_storage_class: 'STANDARD',
+                      region: 'useastr1',
+                      secret: 'SECRETKEY',
+                      multipart_min_part_size: 87877,
+                      multipart_sync_threshold: 987877,
+                      host_style: true
+                    }
+                  }
+                }
+              ]
+            },
+            {
+              name: 'placement1',
+              tier_targets: [
+                {
+                  val: {
+                    storage_class: 'CloudIBM',
+                    tier_type: 'cloud-s3',
+                    s3: {
+                      endpoint: 'https://s3.amazonaws.com',
+                      access_key: 'ACCESSKEY',
+                      storage_class: 'GLACIER',
+                      target_path: '/pathStorage',
+                      target_storage_class: 'CloudIBM',
+                      region: 'useast1',
+                      secret: 'SECRETKEY',
+                      multipart_min_part_size: 187988787,
+                      multipart_sync_threshold: 878787878,
+                      host_style: false
+                    }
+                  }
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    };
+    component.storageClassForm.get('zonegroup').setValue('zonegroup1');
+    component.onZonegroupChange();
+    expect(component.placementTargets).toEqual(['default-placement', 'placement1']);
+    expect(component.storageClassForm.get('placement_target').value).toBe('default-placement');
+  });
+
+  it('should set form values on submit', () => {
+    const storageClassName = 'storageClass1';
+    component.storageClassForm.get('storage_class').setValue(storageClassName);
+    component.storageClassForm.get('zonegroup').setValue('zonegroup1');
+    component.storageClassForm.get('placement_target').setValue('placement1');
+    component.storageClassForm.get('endpoint').setValue('http://ams03.com');
+    component.storageClassForm.get('access_key').setValue('accesskey');
+    component.storageClassForm.get('secret_key').setValue('secretkey');
+    component.storageClassForm.get('target_path').setValue('/target');
+    component.storageClassForm.get('retain_head_object').setValue(true);
+    component.storageClassForm.get('region').setValue('useast1');
+    component.storageClassForm.get('multipart_sync_threshold').setValue(1024);
+    component.storageClassForm.get('multipart_min_part_size').setValue(256);
+    component.goToListView();
+    component.submitAction();
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.ts
new file mode 100644 (file)
index 0000000..ed46770
--- /dev/null
@@ -0,0 +1,202 @@
+import { Component, OnInit } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import _ from 'lodash';
+import { Router } from '@angular/router';
+import { RgwStorageClassService } from '~/app/shared/api/rgw-storage-class.service';
+import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
+import {
+  CLOUD_TIER,
+  DEFAULT_PLACEMENT,
+  RequestModel,
+  Target,
+  ZoneGroup,
+  ZoneGroupDetails
+} from '../models/rgw-storage-class.model';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+  selector: 'cd-rgw-storage-class-form',
+  templateUrl: './rgw-storage-class-form.component.html',
+  styleUrls: ['./rgw-storage-class-form.component.scss']
+})
+export class RgwStorageClassFormComponent extends CdForm implements OnInit {
+  storageClassForm: CdFormGroup;
+  action: string;
+  resource: string;
+  targetPathText: string;
+  targetEndpointText: string;
+  targetRegionText: string;
+  showAdvanced: boolean = false;
+  defaultZoneGroup: string;
+  zonegroupNames: ZoneGroup[];
+  placementTargets: string[] = [];
+  multipartMinPartText: string;
+  multipartSyncThreholdText: string;
+  selectedZoneGroup: string;
+  defaultZonegroup: ZoneGroup;
+  zoneGroupDeatils: ZoneGroupDetails;
+  targetSecretKeyText: string;
+  targetAccessKeyText: string;
+  retainHeadObjectText: string;
+
+  constructor(
+    public actionLabels: ActionLabelsI18n,
+    private formBuilder: CdFormBuilder,
+    private notificationService: NotificationService,
+    private rgwStorageService: RgwStorageClassService,
+    private rgwZoneGroupService: RgwZonegroupService,
+    private router: Router
+  ) {
+    super();
+    this.resource = $localize`Tiering Storage Class`;
+  }
+
+  ngOnInit() {
+    this.multipartMinPartText =
+      'It specifies that objects this size or larger are transitioned to the cloud using multipart upload.';
+    this.multipartSyncThreholdText =
+      'It specifies the minimum part size to use when transitioning objects using multipart upload.';
+    this.targetPathText =
+      'Target Path refers to the storage location (e.g., bucket or container) in the cloud where data will be stored.';
+    this.targetRegionText = 'The region of the remote cloud service where storage is located.';
+    this.targetEndpointText = 'The URL endpoint of the remote cloud service for accessing storage.';
+    this.targetAccessKeyText =
+      "To view or copy your access key, go to your cloud service's user management or credentials section, find your user profile, and locate the access key. You can view and copy the key by following the instructions provided.";
+
+    this.targetSecretKeyText =
+      "To view or copy your secret key, go to your cloud service's user management or credentials section, find your user profile, and locate the access key. You can view and copy the key by following the instructions provided.";
+    this.retainHeadObjectText =
+      'Retain object metadata after transition to the cloud (default: deleted).';
+    this.action = this.actionLabels.CREATE;
+    this.createForm();
+    this.loadZoneGroup();
+  }
+
+  createForm() {
+    this.storageClassForm = this.formBuilder.group({
+      storage_class: new FormControl('', {
+        validators: [Validators.required]
+      }),
+      zonegroup: new FormControl(this.selectedZoneGroup, {
+        validators: [Validators.required]
+      }),
+      region: new FormControl('', {
+        validators: [Validators.required]
+      }),
+      placement_target: new FormControl('', {
+        validators: [Validators.required]
+      }),
+      endpoint: new FormControl(null, {
+        validators: [Validators.required]
+      }),
+      access_key: new FormControl(null, Validators.required),
+      secret_key: new FormControl(null, Validators.required),
+      target_path: new FormControl('', {
+        validators: [Validators.required]
+      }),
+      retain_head_object: new FormControl(false),
+      multipart_sync_threshold: new FormControl(33554432),
+      multipart_min_part_size: new FormControl(33554432)
+    });
+  }
+
+  loadZoneGroup(): Promise<void> {
+    return new Promise((resolve, reject) => {
+      this.rgwZoneGroupService.getAllZonegroupsInfo().subscribe(
+        (data: ZoneGroupDetails) => {
+          this.zoneGroupDeatils = data;
+          this.zonegroupNames = [];
+          this.placementTargets = [];
+          if (data.zonegroups && data.zonegroups.length > 0) {
+            this.zonegroupNames = data.zonegroups.map((zoneGroup: ZoneGroup) => {
+              return {
+                id: zoneGroup.id,
+                name: zoneGroup.name
+              };
+            });
+          }
+          this.defaultZonegroup = this.zonegroupNames.find(
+            (zonegroups: ZoneGroup) => zonegroups.id === data.default_zonegroup
+          );
+
+          this.storageClassForm.get('zonegroup').setValue(this.defaultZonegroup.name);
+          this.onZonegroupChange();
+          resolve();
+        },
+        (error) => reject(error)
+      );
+    });
+  }
+
+  onZonegroupChange() {
+    const zoneGroupControl = this.storageClassForm.get('zonegroup').value;
+    const selectedZoneGroup = this.zoneGroupDeatils.zonegroups.find(
+      (zonegroup) => zonegroup.name === zoneGroupControl
+    );
+    const defaultPlacementTarget = selectedZoneGroup.placement_targets.find(
+      (target: Target) => target.name === DEFAULT_PLACEMENT
+    );
+    if (selectedZoneGroup) {
+      const placementTargetNames = selectedZoneGroup.placement_targets.map(
+        (target: Target) => target.name
+      );
+      this.placementTargets = placementTargetNames;
+    }
+    if (defaultPlacementTarget) {
+      this.storageClassForm.get('placement_target').setValue(defaultPlacementTarget.name);
+    }
+  }
+
+  submitAction() {
+    const component = this;
+    const requestModel = this.buildRequest();
+    const storageclassName = this.storageClassForm.get('storage_class').value;
+    this.rgwStorageService.createStorageClass(requestModel).subscribe(
+      () => {
+        this.notificationService.show(
+          NotificationType.success,
+          $localize`Created Storage Class '${storageclassName}'`
+        );
+        this.goToListView();
+      },
+      () => {
+        component.storageClassForm.setErrors({ cdSubmitButton: true });
+      }
+    );
+  }
+
+  goToListView() {
+    this.router.navigate([`rgw/tiering`]);
+  }
+
+  buildRequest() {
+    const rawFormValue = _.cloneDeep(this.storageClassForm.value);
+    const requestModel: RequestModel = {
+      zone_group: rawFormValue.zonegroup,
+      placement_targets: [
+        {
+          tags: [],
+          placement_id: rawFormValue.placement_target,
+          storage_class: rawFormValue.storage_class,
+          tier_type: CLOUD_TIER,
+          tier_config: {
+            endpoint: rawFormValue.endpoint,
+            access_key: rawFormValue.access_key,
+            secret: rawFormValue.secret_key,
+            target_path: rawFormValue.target_path,
+            retain_head_object: rawFormValue.retain_head_object,
+            region: rawFormValue.region,
+            multipart_sync_threshold: rawFormValue.multipart_sync_threshold,
+            multipart_min_part_size: rawFormValue.multipart_min_part_size
+          }
+        }
+      ]
+    };
+    return requestModel;
+  }
+}
index 5ea1dd1b59479c8897f99e2adc1ebc83d17ccc58..16afef45d368916938cca8b53f66ec2b4c7ffbc1 100644 (file)
@@ -3,7 +3,6 @@ import { CdTableAction } from '~/app/shared/models/cd-table-action';
 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
 
-import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
 import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
 import {
   StorageClass,
@@ -14,19 +13,26 @@ import {
   ZoneGroupDetails
 } from '../models/rgw-storage-class.model';
 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { FinishedTask } from '~/app/shared/models/finished-task';
 import { Icons } from '~/app/shared/enum/icons.enum';
 import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
 import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
-import { FinishedTask } from '~/app/shared/models/finished-task';
 import { RgwStorageClassService } from '~/app/shared/api/rgw-storage-class.service';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
 import { Permission } from '~/app/shared/models/permissions';
 
+import { Router } from '@angular/router';
+
+const BASE_URL = 'rgw/tiering';
+
 @Component({
   selector: 'cd-rgw-storage-class-list',
   templateUrl: './rgw-storage-class-list.component.html',
-  styleUrls: ['./rgw-storage-class-list.component.scss']
+  styleUrls: ['./rgw-storage-class-list.component.scss'],
+  providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
 })
 export class RgwStorageClassListComponent extends ListWithDetails implements OnInit {
   columns: CdTableColumn[];
@@ -41,7 +47,9 @@ export class RgwStorageClassListComponent extends ListWithDetails implements OnI
     private cdsModalService: ModalCdsService,
     private taskWrapper: TaskWrapperService,
     private authStorageService: AuthStorageService,
-    private rgwStorageClassService: RgwStorageClassService
+    private rgwStorageClassService: RgwStorageClassService,
+    private router: Router,
+    private urlBuilder: URLBuilderService
   ) {
     super();
     this.permission = this.authStorageService.getPermissions().rgw;
@@ -76,6 +84,13 @@ export class RgwStorageClassListComponent extends ListWithDetails implements OnI
       }
     ];
     this.tableActions = [
+      {
+        name: this.actionLabels.CREATE,
+        permission: 'create',
+        icon: Icons.add,
+        click: () => this.router.navigate([this.urlBuilder.getCreate()]),
+        canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+      },
       {
         name: this.actionLabels.REMOVE,
         permission: 'delete',
index 37d4e9c4373a40fb9cc60d1536c8bf39e9151884..6bb87d9ec363c816a0f1a4fc19afad509d915dd5 100644 (file)
@@ -64,6 +64,7 @@ import { RgwMultisiteSyncFlowModalComponent } from './rgw-multisite-sync-flow-mo
 import { RgwMultisiteSyncPipeModalComponent } from './rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component';
 import { RgwMultisiteTabsComponent } from './rgw-multisite-tabs/rgw-multisite-tabs.component';
 import { RgwStorageClassListComponent } from './rgw-storage-class-list/rgw-storage-class-list.component';
+
 import {
   ButtonModule,
   GridModule,
@@ -77,13 +78,15 @@ import {
   TreeviewModule,
   SelectModule,
   NumberModule,
-  TabsModule
+  TabsModule,
+  AccordionModule
 } from 'carbon-components-angular';
 import { CephSharedModule } from '../shared/ceph-shared.module';
 import { RgwUserAccountsComponent } from './rgw-user-accounts/rgw-user-accounts.component';
 import { RgwUserAccountsFormComponent } from './rgw-user-accounts-form/rgw-user-accounts-form.component';
 import { RgwUserAccountsDetailsComponent } from './rgw-user-accounts-details/rgw-user-accounts-details.component';
 import { RgwStorageClassDetailsComponent } from './rgw-storage-class-details/rgw-storage-class-details.component';
+import { RgwStorageClassFormComponent } from './rgw-storage-class-form/rgw-storage-class-form.component';
 
 @NgModule({
   imports: [
@@ -111,10 +114,13 @@ import { RgwStorageClassDetailsComponent } from './rgw-storage-class-details/rgw
     IconModule,
     NgbProgressbar,
     InputModule,
+    AccordionModule,
     CheckboxModule,
     SelectModule,
     NumberModule,
-    TabsModule
+    TabsModule,
+    IconModule,
+    SelectModule
   ],
   exports: [
     RgwDaemonListComponent,
@@ -171,7 +177,8 @@ import { RgwStorageClassDetailsComponent } from './rgw-storage-class-details/rgw
     RgwUserAccountsFormComponent,
     RgwUserAccountsDetailsComponent,
     RgwStorageClassListComponent,
-    RgwStorageClassDetailsComponent
+    RgwStorageClassDetailsComponent,
+    RgwStorageClassFormComponent
   ],
   providers: [TitleCasePipe]
 })
@@ -325,7 +332,14 @@ const routes: Routes = [
   {
     path: 'tiering',
     data: { breadcrumbs: 'Tiering' },
-    children: [{ path: '', component: RgwStorageClassListComponent }]
+    children: [
+      { path: '', component: RgwStorageClassListComponent },
+      {
+        path: URLVerbs.CREATE,
+        component: RgwStorageClassFormComponent,
+        data: { breadcrumbs: ActionLabels.CREATE }
+      }
+    ]
   },
   {
     path: 'nfs',
index dc8eef51691c86ea7a1a3b9018c14a0473eb9a7e..ac65bfc424da3897ddf1aa7a560ad8cf42fb28f2 100644 (file)
@@ -4,6 +4,8 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/
 import { RgwStorageClassService } from './rgw-storage-class.service';
 import { configureTestBed } from '~/testing/unit-test-helper';
 
+import { RequestModel } from '~/app/ceph/rgw/models/rgw-storage-class.model';
+
 describe('RgwStorageClassService', () => {
   let service: RgwStorageClassService;
   let httpTesting: HttpTestingController;
@@ -29,4 +31,31 @@ describe('RgwStorageClassService', () => {
     );
     expect(req.request.method).toBe('DELETE');
   });
+
+  it('should call create', () => {
+    const request: RequestModel = {
+      zone_group: 'default',
+      placement_targets: [
+        {
+          tags: [],
+          placement_id: 'default-placement',
+          storage_class: 'test1',
+          tier_type: 'cloud-s3',
+          tier_config: {
+            endpoint: 'http://198.162.100.100:80',
+            access_key: 'test56',
+            secret: 'test56',
+            target_path: 'tsest-dnyanee',
+            retain_head_object: false,
+            region: 'ams3d',
+            multipart_sync_threshold: 33554432,
+            multipart_min_part_size: 33554432
+          }
+        }
+      ]
+    };
+    service.createStorageClass(request).subscribe();
+    const req = httpTesting.expectOne('api/rgw/zonegroup/storage-class');
+    expect(req.request.method).toBe('POST');
+  });
 });
index 52d0f7c9326f2554b3ec83c10df014092ee43db1..9c3e1fcdbcd58648b01c71e44d5a3554f068906c 100644 (file)
@@ -1,16 +1,22 @@
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
+import { RequestModel } from '~/app/ceph/rgw/models/rgw-storage-class.model';
+
 @Injectable({
   providedIn: 'root'
 })
 export class RgwStorageClassService {
-  private url = 'api/rgw/zonegroup';
+  private url = 'api/rgw/zonegroup/storage-class';
 
   constructor(private http: HttpClient) {}
 
   removeStorageClass(placement_target: string, storage_class: string) {
-    return this.http.delete(`${this.url}/storage-class/${placement_target}/${storage_class}`, {
+    return this.http.delete(`${this.url}/${placement_target}/${storage_class}`, {
       observe: 'response'
     });
   }
+
+  createStorageClass(requestModel: RequestModel) {
+    return this.http.post(`${this.url}`, requestModel);
+  }
 }
index bae4c03d2c0aa0b3b16f90e0733756b52227c9ad..c797dbdcfec840ed6e8c523d4a468edd16318f60 100644 (file)
@@ -17,6 +17,7 @@ import { StatefulTabDirective } from './stateful-tab.directive';
 import { TrimDirective } from './trim.directive';
 import { RequiredFieldDirective } from './required-field.directive';
 import { ReactiveFormsModule } from '@angular/forms';
+import { OptionalFieldDirective } from './optional-field.directive';
 
 @NgModule({
   imports: [ReactiveFormsModule],
@@ -36,7 +37,8 @@ import { ReactiveFormsModule } from '@angular/forms';
     CdFormGroupDirective,
     CdFormValidationDirective,
     AuthStorageDirective,
-    RequiredFieldDirective
+    RequiredFieldDirective,
+    OptionalFieldDirective
   ],
   exports: [
     AutofocusDirective,
@@ -54,7 +56,8 @@ import { ReactiveFormsModule } from '@angular/forms';
     CdFormGroupDirective,
     CdFormValidationDirective,
     AuthStorageDirective,
-    RequiredFieldDirective
+    RequiredFieldDirective,
+    OptionalFieldDirective
   ]
 })
 export class DirectivesModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/optional-field.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/optional-field.directive.spec.ts
new file mode 100644 (file)
index 0000000..5890944
--- /dev/null
@@ -0,0 +1,9 @@
+import { ElementRef } from '@angular/core';
+import { OptionalFieldDirective } from './optional-field.directive';
+
+describe('OptionalFieldDirective', () => {
+  it('should create an instance', () => {
+    const directive = new OptionalFieldDirective(new ElementRef(''), null);
+    expect(directive).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/optional-field.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/optional-field.directive.ts
new file mode 100644 (file)
index 0000000..aeb6a39
--- /dev/null
@@ -0,0 +1,19 @@
+import { AfterViewInit, Directive, ElementRef, Input, Renderer2 } from '@angular/core';
+
+@Directive({
+  selector: '[cdOptionalField]'
+})
+export class OptionalFieldDirective implements AfterViewInit {
+  @Input('cdOptionalField') label: string;
+  @Input() skeleton: boolean;
+  constructor(private elementRef: ElementRef, private renderer: Renderer2) {}
+
+  ngAfterViewInit() {
+    if (!this.label || this.skeleton) return;
+    const labelElement = this.elementRef.nativeElement.querySelector('.cds--label');
+
+    if (labelElement) {
+      this.renderer.setProperty(labelElement, 'textContent', `${this.label} (optional)`);
+    }
+  }
+}
index 2ef61bbfcc1bc3ea29b288a7400fb372a7ba6755..88707c0e49ac8665c7a1460bffe1488c3f4696f9 100644 (file)
@@ -13820,6 +13820,46 @@ paths:
       - jwt: []
       tags:
       - RgwZonegroup
+  /api/rgw/zonegroup/storage-class:
+    post:
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                placement_targets:
+                  default: []
+                  type: string
+                zone_group:
+                  type: string
+              required:
+              - zone_group
+              type: object
+      responses:
+        '201':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource created.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      tags:
+      - RgwZonegroup
   /api/rgw/zonegroup/storage-class/{placement_id}/{storage_class}:
     delete:
       parameters:
index ffedf0111957d0f3f7f5981cda54b3afc19851ef..92c23f090e6b4c8dba6994647d87c1984183e5d5 100755 (executable)
@@ -1694,46 +1694,89 @@ class RgwMultisite:
             raise DashboardException(error, http_status_code=500, component='rgw')
         return out
 
-    # If realm list is empty restart RGW daemons else update the period.
-    def handle_rgw_realm(self):
+    # If realm list is empty, restart RGW daemons. Otherwise, update the period.
+    def ensure_realm_and_sync_period(self):
         rgw_realm_list = self.list_realms()
         if len(rgw_realm_list['realms']) < 1:
             rgw_service_manager = RgwServiceManager()
-            rgw_service_manager.restart_rgw_daemons_and_set_credentials()
+            rgw_service_manager.restart_rgw_daemons()
         else:
             self.update_period()
 
     def add_placement_targets(self, zonegroup_name: str, placement_targets: List[Dict]):
         rgw_add_placement_cmd = ['zonegroup', 'placement', 'add']
-        for placement_target in placement_targets:
-            cmd_add_placement_options = ['--rgw-zonegroup', zonegroup_name,
-                                         '--placement-id', placement_target['placement_id']]
-            if placement_target['tags']:
+        STANDARD_STORAGE_CLASS = "STANDARD"
+        CLOUD_S3_TIER_TYPE = "cloud-s3"
+
+        for placement_target in placement_targets:  # pylint: disable=R1702
+            cmd_add_placement_options = [
+                '--rgw-zonegroup', zonegroup_name,
+                '--placement-id', placement_target['placement_id']
+            ]
+            storage_class_name = placement_target.get('storage_class', None)
+
+            if (
+                placement_target.get('tier_type') == CLOUD_S3_TIER_TYPE
+                and storage_class_name != STANDARD_STORAGE_CLASS
+            ):
+                tier_config = placement_target.get('tier_config', {})
+                if tier_config:
+                    tier_config_items = (
+                        f'{key}={value}' for key, value in tier_config.items()
+                    )
+                    tier_config_str = ','.join(tier_config_items)
+                    cmd_add_placement_options += [
+                        '--tier-type', 'cloud-s3', '--tier-config', tier_config_str
+                    ]
+
+            if placement_target.get('tags') and storage_class_name != STANDARD_STORAGE_CLASS:
                 cmd_add_placement_options += ['--tags', placement_target['tags']]
+
+            storage_classes = (
+                placement_target['storage_class'].split(",")
+                if placement_target['storage_class']
+                else []
+            )
             rgw_add_placement_cmd += cmd_add_placement_options
-            try:
-                exit_code, _, err = mgr.send_rgwadmin_command(rgw_add_placement_cmd)
-                if exit_code > 0:
-                    raise DashboardException(e=err,
-                                             msg='Unable to add placement target {} to zonegroup {}'.format(placement_target['placement_id'], zonegroup_name),  # noqa E501  #pylint: disable=line-too-long
-                                             http_status_code=500, component='rgw')
-            except SubprocessError as error:
-                raise DashboardException(error, http_status_code=500, component='rgw')
-            self.update_period()
-            storage_classes = placement_target['storage_class'].split(",") if placement_target['storage_class'] else []  # noqa E501  #pylint: disable=line-too-long
+
+            if not storage_classes:
+                try:
+                    exit_code, _, err = mgr.send_rgwadmin_command(rgw_add_placement_cmd)
+                    if exit_code > 0:
+                        raise DashboardException(
+                            e=err,
+                            msg=(
+                                f'Unable to add placement target '
+                                f'{placement_target["placement_id"]} '
+                                f'to zonegroup {zonegroup_name}'
+                            )
+                        )
+                except SubprocessError as error:
+                    raise DashboardException(error, http_status_code=500, component='rgw')
+                self.ensure_realm_and_sync_period()
+
             if storage_classes:
                 for sc in storage_classes:
-                    cmd_add_placement_options = ['--storage-class', sc]
-                    try:
-                        exit_code, _, err = mgr.send_rgwadmin_command(
-                            rgw_add_placement_cmd + cmd_add_placement_options)
-                        if exit_code > 0:
-                            raise DashboardException(e=err,
-                                                     msg='Unable to add placement target {} to zonegroup {}'.format(placement_target['placement_id'], zonegroup_name),  # noqa E501  #pylint: disable=line-too-long
-                                                     http_status_code=500, component='rgw')
-                    except SubprocessError as error:
-                        raise DashboardException(error, http_status_code=500, component='rgw')
-                    self.update_period()
+                    if sc == storage_class_name:
+                        cmd_add_placement_options = ['--storage-class', sc]
+                        try:
+                            exit_code, _, err = mgr.send_rgwadmin_command(
+                                rgw_add_placement_cmd + cmd_add_placement_options
+                            )
+                            if exit_code > 0:
+                                raise DashboardException(
+                                    e=err,
+                                    msg=(
+                                        f'Unable to add placement target '
+                                        f'{placement_target["placement_id"]} '
+                                        f'to zonegroup {zonegroup_name}'
+                                    ),
+                                    http_status_code=500,
+                                    component='rgw'
+                                )
+                        except SubprocessError as error:
+                            raise DashboardException(error, http_status_code=500, component='rgw')
+                        self.ensure_realm_and_sync_period()
 
     def modify_placement_targets(self, zonegroup_name: str, placement_targets: List[Dict]):
         rgw_add_placement_cmd = ['zonegroup', 'placement', 'modify']
@@ -1787,7 +1830,7 @@ class RgwMultisite:
         except SubprocessError as error:
             raise DashboardException(error, http_status_code=500, component='rgw')
 
-        self.handle_rgw_realm()
+        self.ensure_realm_and_sync_period()
 
     # pylint: disable=W0102
     def edit_zonegroup(self, realm_name: str, zonegroup_name: str, new_zonegroup_name: str,
index 9b789c0c85929b279fc5a91afca71532764fe02c..679e2919b7df2cb38e71cf5fdc887c512f00f2b5 100644 (file)
@@ -117,24 +117,25 @@ class RgwServiceManager:
         return port
 
     def restart_rgw_daemons_and_set_credentials(self):
-        # Restart RGW daemons and set credentials.
-        logger.info("Restarting RGW daemons and setting credentials")
+        if self.restart_rgw_daemons():
+            logger.info("All daemons are up, configuring RGW credentials")
+            self.configure_rgw_credentials()
+        else:
+            logger.error("Not all daemons are up, skipping RGW credentials configuration")
+
+    def restart_rgw_daemons(self):
+        # Restart RGW daemons
+        logger.info("Restarting RGW daemons")
         orch = OrchClient.instance()
         services, _ = orch.services.list(service_type='rgw', offset=0)
-
         all_daemons_up = True
         for service in services:
             logger.info("Verifying service restart for: %s", service['service_id'])
             daemons_up = verify_service_restart('rgw', service['service_id'])
             if not daemons_up:
-                logger.error("Service %s restart verification failed", service['service_id'])
                 all_daemons_up = False
 
-        if all_daemons_up:
-            logger.info("All daemons are up, configuring RGW credentials")
-            self.configure_rgw_credentials()
-        else:
-            logger.error("Not all daemons are up, skipping RGW credentials configuration")
+        return all_daemons_up
 
     def _parse_secrets(self, user: str, data: dict) -> Tuple[str, str]:
         for key in data.get('keys', []):