]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mg/dashboard: Edit Storage Class 61874/head
authorDnyaneshwari <dnyaneshwari@li-9c9fbecc-2d5c-11b2-a85c-e2a7cc8a424f.ibm.com>
Tue, 11 Feb 2025 06:29:38 +0000 (11:59 +0530)
committerDnyaneshwari <dnyaneshwari@li-9c9fbecc-2d5c-11b2-a85c-e2a7cc8a424f.ibm.com>
Fri, 28 Feb 2025 05:29:22 +0000 (10:59 +0530)
Fixes: https://tracker.ceph.com/issues/70016
Signed-off-by: Dnyaneshwari Talwekar <dtalweka@redhat.com>
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
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.html
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.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rgw_client.py

index ef8903afb082ec3081e2f9f7f720cf913f55c8a0..e315d38732fc8b8ec92d784059e2637c6bdc1bdc 100755 (executable)
@@ -1182,6 +1182,13 @@ class RgwZonegroup(RESTController):
         result = multisite_instance.get_zonegroup(zonegroup_name)
         return result
 
+    @Endpoint('GET')
+    @ReadPermission
+    def get_placement_target_by_placement_id(self, placement_id):
+        multisite_instance = RgwMultisite()
+        result = multisite_instance.get_placement_by_placement_id(placement_id)
+        return result
+
     @Endpoint('DELETE', path='storage-class')
     @DeletePermission
     def remove_storage_class(self, placement_id: str, storage_class: str):
@@ -1197,6 +1204,14 @@ class RgwZonegroup(RESTController):
         result = multisite_instance.add_placement_targets(zone_group, placement_targets)
         return result
 
+    @Endpoint('PUT', path='storage-class')
+    @CreatePermission
+    # pylint: disable=W0102
+    def editStorageClass(self, zone_group, placement_targets: List[Dict[str, str]] = []):
+        multisite_instance = RgwMultisite()
+        result = multisite_instance.modify_placement_targets(zone_group, placement_targets)
+        return result
+
     @Endpoint()
     @ReadPermission
     def get_all_zonegroups_info(self):
index 0e671db749944c9aa870068382adc007ee93c513..85b0bf75d0afa5edd126e26aa075357b96e6cce3 100644 (file)
@@ -9,6 +9,7 @@ export interface StorageClass {
   endpoint: string;
   region: string;
   placement_target: string;
+  zonegroup_name?: string;
 }
 
 export interface StorageClassDetails {
@@ -71,6 +72,7 @@ export interface S3Details {
   multipart_min_part_size: number;
   multipart_sync_threshold: number;
   host_style: boolean;
+  retain_head_object?: boolean;
 }
 
 export interface RequestModel {
@@ -81,7 +83,6 @@ export interface RequestModel {
 export interface PlacementTarget {
   tags: string[];
   placement_id: string;
-  storage_class: string;
   tier_type: typeof CLOUD_TIER;
   tier_config: {
     endpoint: string;
@@ -93,6 +94,9 @@ export interface PlacementTarget {
     multipart_sync_threshold: number;
     multipart_min_part_size: number;
   };
+  storage_class?: string;
+  name?: string;
+  tier_targets?: TierTarget[];
 }
 
 export const CLOUD_TIER = 'cloud-s3';
index 6d38088d627630b377b1920652a65ec8b2ba23a3..35710457f441d1b94d72e288623d916383b7a4be 100644 (file)
 <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>
+  <ng-container *cdFormLoading="loading">
+    <form name="storageClassForm"
+          #formDir="ngForm"
+          [formGroup]="storageClassForm"
+          novalidate>
+      <div i18n="form title"
+           class="form-header">
+        {{ action | titlecase }} {{ resource | upperFirst }}
       </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
+      <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.showError('zonegroup', formDir, 'required')
+            "
+            (change)="onZonegroupChange()"
+            [invalidText]="zonegroupError"
           >
-        </ng-template>
+            <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.showError('placement_target', formDir, 'required')
+            "
+            [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>
-    </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 -->
+      <!-- Storage Class -->
+      <div class="form-item">
         <cds-text-label
-          labelInputID="region"
+          labelInputID="storage_class"
           i18n
+          [disabled]="editing"
           [invalid]="
-            storageClassForm.controls.region.invalid && storageClassForm.controls.region.dirty
+            storageClassForm.controls.storage_class.invalid &&
+            storageClassForm.controls.storage_class.dirty
           "
-          [invalidText]="regionError"
-          [helperText]="targetRegionText"
-          >Target Region
+          [invalidText]="storageError"
+          >Storage Class Name
           <input
             cdsText
-            type="text"
-            id="region"
-            formControlName="region"
-            placeholder="e.g, us-east-1"
-            i18n-placeholder
+            type="type"
+            id="storage_class"
+            formControlName="storage_class"
             [invalid]="
-              storageClassForm.controls.region.invalid && storageClassForm.controls.region.dirty
+            storageClassForm.showError('storage_class', formDir, 'required')
             "
           />
         </cds-text-label>
-        <ng-template #regionError>
+        <ng-template #storageError>
           <span
             class="invalid-feedback"
-            *ngIf="storageClassForm.showError('region', formDir, 'required')"
+            *ngIf="storageClassForm.showError('storage_class', 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"
+      <div class="form-item form-item-append"
+           cdsRow>
+        <div cdsCol>
+          <!-- Target Region -->
+          <cds-text-label
+            labelInputID="region"
+            i18n
             [invalid]="
-              storageClassForm.controls.endpoint.invalid && storageClassForm.controls.endpoint.dirty
+            storageClassForm.showError('region', formDir, 'required')
             "
-          />
-        </cds-text-label>
-        <ng-template #endpointError>
-          <span
-            class="invalid-feedback"
-            *ngIf="storageClassForm.showError('endpoint', formDir, 'required')"
+            [invalidText]="regionError"
+            [helperText]="targetRegionText"
+            >Target Region
+            <input
+              cdsText
+              type="text"
+              id="region"
+              formControlName="region"
+              placeholder="e.g, us-east-1"
+              i18n-placeholder
+              [invalid]="
+              storageClassForm.showError('region', formDir, 'required')
+              "
+            />
+          </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
-            >This field is required.</span
-          >
-        </ng-template>
+            [invalid]="
+            storageClassForm.showError('endpoint', formDir, 'required')
+            "
+            [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.showError('endpoint', formDir, 'required')
+              "
+            />
+          </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>
-    </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"
+      <!-- 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
+            storageClassForm.showError('access_key', formDir, 'required')
             "
-          />
-        </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')"
+            [invalidText]="accessError"
+            [helperText]="targetAccessKeyText"
             i18n
-            >This field is required.</span
-          >
-        </ng-template>
+            >Target Access Key
+            <input
+              cdsPassword
+              type="password"
+              id="access_key"
+              formControlName="access_key"
+              [invalid]="
+              storageClassForm.showError('access_key', formDir, 'required')
+              "
+            />
+          </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.showError('secret_key', formDir, 'required')
+            "
+            [invalidText]="secretError"
+            i18n
+            >Target Secret Key
+            <input
+              cdsPassword
+              type="password"
+              id="secret_key"
+              formControlName="secret_key"
+              [invalid]="
+              storageClassForm.showError('secret_key', formDir, 'required')
+              "
+            />
+          </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>
-    </div>
 
-    <!-- Secret Key  -->
-    <div class="form-item">
-      <div cdsCol
-           [columnNumbers]="{ md: 12 }"
-           class="d-flex">
-        <cds-password-label
-          labelInputID="secret_key"
-          [helperText]="targetSecretKeyText"
+      <!-- Target Path -->
+      <div class="form-item">
+        <cds-text-label
+          labelInputID="target_path"
+          i18n
           [invalid]="
-            !storageClassForm.controls.secret_key.valid &&
-            storageClassForm.controls.secret_key.dirty
+          storageClassForm.showError('target_path', formDir, 'required')
           "
-          [invalidText]="secretError"
-          i18n
-          >Target Secret Key
+          [invalidText]="targetError"
+          [helperText]="targetPathText"
+          >Target Path
           <input
-            cdsPassword
-            type="password"
-            id="secret_key"
-            formControlName="secret_key"
+            cdsText
+            type="text"
+            id="target_path"
+            formControlName="target_path"
             [invalid]="
-              !storageClassForm.controls.secret_key.valid &&
-              storageClassForm.controls.secret_key.dirty
+            storageClassForm.showError('target_path', formDir, 'required')
             "
           />
-        </cds-password-label>
-        <cd-copy-2-clipboard-button class="clipboard"> </cd-copy-2-clipboard-button>
-        <ng-template #secretError>
+        </cds-text-label>
+        <ng-template #targetError>
           <span
             class="invalid-feedback"
-            *ngIf="storageClassForm.showError('secret_key', formDir, 'required')"
+            *ngIf="storageClassForm.showError('target_path', 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>
+      <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 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 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>
-          </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>
+          </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>
+  </ng-container>
 </div>
index 2f1c43ca6c168ed500c1cbc73b11d9a793f7f3e9..c6582e46af2c867f11b8448a3ed1e96ea2ae1d8d 100644 (file)
@@ -56,7 +56,7 @@ describe('RgwStorageClassFormComponent', () => {
   });
 
   it('on zonegroup changes', () => {
-    component.zoneGroupDeatils = {
+    component.zoneGroupDetails = {
       default_zonegroup: 'zonegroup1',
       name: 'zonegrp1',
       zonegroups: [
index ed4677035f3216bc0c0c0d72adce79cb53a315b2..d380f08bcc0f65191c6681fba1604d28546acb0b 100644 (file)
@@ -1,18 +1,21 @@
 import { Component, OnInit } from '@angular/core';
 import { FormControl, Validators } from '@angular/forms';
-import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { ActionLabelsI18n, URLVerbs } 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 { ActivatedRoute, 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,
+  PlacementTarget,
   RequestModel,
+  StorageClass,
   Target,
+  TierTarget,
   ZoneGroup,
   ZoneGroupDetails
 } from '../models/rgw-storage-class.model';
@@ -28,6 +31,7 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
   storageClassForm: CdFormGroup;
   action: string;
   resource: string;
+  editing: boolean;
   targetPathText: string;
   targetEndpointText: string;
   targetRegionText: string;
@@ -39,10 +43,12 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
   multipartSyncThreholdText: string;
   selectedZoneGroup: string;
   defaultZonegroup: ZoneGroup;
-  zoneGroupDeatils: ZoneGroupDetails;
+  zoneGroupDetails: ZoneGroupDetails;
   targetSecretKeyText: string;
   targetAccessKeyText: string;
   retainHeadObjectText: string;
+  storageClassInfo: StorageClass;
+  tierTargetInfo: TierTarget;
 
   constructor(
     public actionLabels: ActionLabelsI18n,
@@ -50,10 +56,13 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
     private notificationService: NotificationService,
     private rgwStorageService: RgwStorageClassService,
     private rgwZoneGroupService: RgwZonegroupService,
-    private router: Router
+    private router: Router,
+    private route: ActivatedRoute
   ) {
     super();
     this.resource = $localize`Tiering Storage Class`;
+    this.editing = this.router.url.startsWith(`/rgw/tiering/${URLVerbs.EDIT}`);
+    this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
   }
 
   ngOnInit() {
@@ -72,9 +81,45 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
       "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.loadingReady();
     this.loadZoneGroup();
+    if (this.editing) {
+      this.route.params.subscribe((params: StorageClass) => {
+        this.storageClassInfo = params;
+      });
+      this.rgwStorageService
+        .getPlacement_target(this.storageClassInfo.placement_target)
+        .subscribe((placementTargetInfo: PlacementTarget) => {
+          this.tierTargetInfo = this.getTierTargetByStorageClass(
+            placementTargetInfo,
+            this.storageClassInfo.storage_class
+          );
+          let response = this.tierTargetInfo.val.s3;
+          this.storageClassForm.get('zonegroup').disable();
+          this.storageClassForm.get('placement_target').disable();
+          this.storageClassForm.get('storage_class').disable();
+          this.storageClassForm.get('zonegroup').setValue(this.storageClassInfo.zonegroup_name);
+          this.storageClassForm.get('region').setValue(response.region);
+          this.storageClassForm
+            .get('placement_target')
+            .setValue(this.storageClassInfo.placement_target);
+          this.storageClassForm.get('endpoint').setValue(response.endpoint);
+          this.storageClassForm.get('storage_class').setValue(this.storageClassInfo.storage_class);
+          this.storageClassForm.get('access_key').setValue(response.access_key);
+          this.storageClassForm.get('secret_key').setValue(response.access_key);
+          this.storageClassForm.get('target_path').setValue(response.target_path);
+          this.storageClassForm
+            .get('retain_head_object')
+            .setValue(response.retain_head_object || false);
+          this.storageClassForm
+            .get('multipart_sync_threshold')
+            .setValue(response.multipart_sync_threshold || '');
+          this.storageClassForm
+            .get('multipart_min_part_size')
+            .setValue(response.multipart_min_part_size || '');
+        });
+    }
   }
 
   createForm() {
@@ -109,7 +154,7 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
     return new Promise((resolve, reject) => {
       this.rgwZoneGroupService.getAllZonegroupsInfo().subscribe(
         (data: ZoneGroupDetails) => {
-          this.zoneGroupDeatils = data;
+          this.zoneGroupDetails = data;
           this.zonegroupNames = [];
           this.placementTargets = [];
           if (data.zonegroups && data.zonegroups.length > 0) {
@@ -135,7 +180,7 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
 
   onZonegroupChange() {
     const zoneGroupControl = this.storageClassForm.get('zonegroup').value;
-    const selectedZoneGroup = this.zoneGroupDeatils.zonegroups.find(
+    const selectedZoneGroup = this.zoneGroupDetails.zonegroups.find(
       (zonegroup) => zonegroup.name === zoneGroupControl
     );
     const defaultPlacementTarget = selectedZoneGroup.placement_targets.find(
@@ -147,8 +192,12 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
       );
       this.placementTargets = placementTargetNames;
     }
-    if (defaultPlacementTarget) {
+    if (defaultPlacementTarget && !this.editing) {
       this.storageClassForm.get('placement_target').setValue(defaultPlacementTarget.name);
+    } else {
+      this.storageClassForm
+        .get('placement_target')
+        .setValue(this.storageClassInfo.placement_target);
     }
   }
 
@@ -156,33 +205,58 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit {
     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 });
-      }
-    );
+    if (this.editing) {
+      this.rgwStorageService.editStorageClass(requestModel).subscribe(
+        () => {
+          this.notificationService.show(
+            NotificationType.success,
+            $localize`Updated Storage Class '${storageclassName}'`
+          );
+          this.goToListView();
+        },
+        () => {
+          component.storageClassForm.setErrors({ cdSubmitButton: true });
+        }
+      );
+    } else {
+      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`]);
   }
 
+  getTierTargetByStorageClass(placementTargetInfo: PlacementTarget, storageClass: string) {
+    const tierTarget = placementTargetInfo.tier_targets.find(
+      (target: TierTarget) => target.val.storage_class === storageClass
+    );
+    return tierTarget;
+  }
+
   buildRequest() {
     const rawFormValue = _.cloneDeep(this.storageClassForm.value);
+    const zoneGroup = this.storageClassForm.get('zonegroup').value;
+    const storageClass = this.storageClassForm.get('storage_class').value;
+    const placementId = this.storageClassForm.get('placement_target').value;
     const requestModel: RequestModel = {
-      zone_group: rawFormValue.zonegroup,
+      zone_group: zoneGroup,
       placement_targets: [
         {
           tags: [],
-          placement_id: rawFormValue.placement_target,
-          storage_class: rawFormValue.storage_class,
+          placement_id: placementId,
+          storage_class: storageClass,
           tier_type: CLOUD_TIER,
           tier_config: {
             endpoint: rawFormValue.endpoint,
index a0efeff62f2cfb4ae61b7f01a16f3c76c3dbee87..cdea55a32e560fd10e00dc077c32dd9d56e49e49 100644 (file)
@@ -1,3 +1,8 @@
+<legend>
+  <cd-help-text i18n>
+    A storage class for tiering defines the policies for automatically moving objects between different storage tiers.
+  </cd-help-text>
+</legend>
 <cd-table
   [data]="storageClassList"
   columnMode="flex"
index 16afef45d368916938cca8b53f66ec2b4c7ffbc1..a44535d7b4efa8a31fa9ea2ab2e6761c21fddd19 100644 (file)
@@ -27,7 +27,6 @@ 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',
@@ -57,6 +56,11 @@ export class RgwStorageClassListComponent extends ListWithDetails implements OnI
 
   ngOnInit() {
     this.columns = [
+      {
+        name: $localize`Storage Class`,
+        prop: 'storage_class',
+        flexGrow: 2
+      },
       {
         name: $localize`Zone Group`,
         prop: 'zonegroup_name',
@@ -67,11 +71,6 @@ export class RgwStorageClassListComponent extends ListWithDetails implements OnI
         prop: 'placement_target',
         flexGrow: 2
       },
-      {
-        name: $localize`Storage Class`,
-        prop: 'storage_class',
-        flexGrow: 2
-      },
       {
         name: $localize`Target Region`,
         prop: 'region',
@@ -83,6 +82,11 @@ export class RgwStorageClassListComponent extends ListWithDetails implements OnI
         flexGrow: 2
       }
     ];
+    const getStorageUri = () =>
+      this.selection.first() &&
+      `${encodeURI(this.selection.first().zonegroup_name)}/${encodeURI(
+        this.selection.first().placement_target
+      )}/${encodeURI(this.selection.first().storage_class)}`;
     this.tableActions = [
       {
         name: this.actionLabels.CREATE,
@@ -91,6 +95,12 @@ export class RgwStorageClassListComponent extends ListWithDetails implements OnI
         click: () => this.router.navigate([this.urlBuilder.getCreate()]),
         canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
       },
+      {
+        name: this.actionLabels.EDIT,
+        permission: 'update',
+        icon: Icons.edit,
+        routerLink: () => [`/rgw/tiering/edit/${getStorageUri()}`]
+      },
       {
         name: this.actionLabels.REMOVE,
         permission: 'delete',
index 6bb87d9ec363c816a0f1a4fc19afad509d915dd5..77bb10fb077a62727e10fea6615af54a65ec12f0 100644 (file)
@@ -338,6 +338,11 @@ const routes: Routes = [
         path: URLVerbs.CREATE,
         component: RgwStorageClassFormComponent,
         data: { breadcrumbs: ActionLabels.CREATE }
+      },
+      {
+        path: `${URLVerbs.EDIT}/:zonegroup_name/:placement_target/:storage_class`,
+        component: RgwStorageClassFormComponent,
+        data: { breadcrumbs: ActionLabels.EDIT }
       }
     ]
   },
index 9c3e1fcdbcd58648b01c71e44d5a3554f068906c..b6b3cbc3867c7c1d62e24ddde9b459c44f73c787 100644 (file)
@@ -6,7 +6,8 @@ import { RequestModel } from '~/app/ceph/rgw/models/rgw-storage-class.model';
   providedIn: 'root'
 })
 export class RgwStorageClassService {
-  private url = 'api/rgw/zonegroup/storage-class';
+  private baseUrl = 'api/rgw/zonegroup';
+  private url = `${this.baseUrl}/storage-class`;
 
   constructor(private http: HttpClient) {}
 
@@ -19,4 +20,12 @@ export class RgwStorageClassService {
   createStorageClass(requestModel: RequestModel) {
     return this.http.post(`${this.url}`, requestModel);
   }
+
+  editStorageClass(requestModel: RequestModel) {
+    return this.http.put(`${this.url}`, requestModel);
+  }
+
+  getPlacement_target(placement_id: string) {
+    return this.http.get(`${this.baseUrl}/get_placement_target_by_placement_id/${placement_id}`);
+  }
 }
index c4fb7fc9465bdef28fcd254c6b20b759f78d27e1..8b6bdb57ed1edf0233795369112bcaf2c251cad3 100644 (file)
@@ -13820,6 +13820,33 @@ paths:
       - jwt: []
       tags:
       - RgwZonegroup
+  /api/rgw/zonegroup/get_placement_target_by_placement_id/{placement_id}:
+    get:
+      parameters:
+      - in: path
+        name: placement_id
+        required: true
+        schema:
+          type: string
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: OK
+        '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:
     post:
       parameters: []
@@ -13860,6 +13887,45 @@ paths:
       - jwt: []
       tags:
       - RgwZonegroup
+    put:
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                placement_targets:
+                  default: []
+                  type: string
+                zone_group:
+                  type: string
+              required:
+              - zone_group
+              type: object
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource updated.
+        '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 92c23f090e6b4c8dba6994647d87c1984183e5d5..40007a08ec397624142898c0bdd169a09fa3bf36 100755 (executable)
@@ -1694,6 +1694,19 @@ class RgwMultisite:
             raise DashboardException(error, http_status_code=500, component='rgw')
         return out
 
+    def get_placement_by_placement_id(self, placement_id: str):
+        radosgw_get_placement_cmd = ['zonegroup', 'placement',
+                                     'get', '--placement-id', placement_id]
+        try:
+            exit_code, out, err = mgr.send_rgwadmin_command(radosgw_get_placement_cmd)
+            if exit_code > 0:
+                raise DashboardException(e=err, msg='Unable to get placement details by id',
+                                         http_status_code=500, component='rgw')
+
+        except SubprocessError as error:
+            raise DashboardException(error, http_status_code=500, component='rgw')
+        return out
+
     # 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()
@@ -1780,37 +1793,78 @@ class RgwMultisite:
 
     def modify_placement_targets(self, zonegroup_name: str, placement_targets: List[Dict]):
         rgw_add_placement_cmd = ['zonegroup', 'placement', 'modify']
-        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,line-too-long # noqa: E501
+            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
-            storage_classes = placement_target['storage_class'].split(",") if placement_target['storage_class'] else []  # noqa E501  #pylint: disable=line-too-long
-            if storage_classes:
-                for sc in storage_classes:
-                    cmd_add_placement_options = []
-                    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()
-            else:
+
+            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='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')
+                        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.update_period()
+                self.ensure_realm_and_sync_period()
+
+            if storage_classes:
+                for sc in storage_classes:
+                    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 delete_placement_targets(self, placement_id: str, storage_class: str):
         rgw_zonegroup_delete_cmd = ['zonegroup', 'placement', 'rm',