]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Edit a service feature 43903/head
authorNizamudeen A <nia@redhat.com>
Fri, 12 Nov 2021 08:14:51 +0000 (13:44 +0530)
committerNizamudeen A <nia@redhat.com>
Fri, 12 Nov 2021 08:14:51 +0000 (13:44 +0530)
Fixes: https://tracker.ceph.com/issues/53077
Signed-off-by: Nizamudeen A <nia@redhat.com>
14 files changed:
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/services.po.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/05-services.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/04-create-cluster-create-services.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/orchestrator.enum.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts
src/pybind/mgr/dashboard/services/orchestrator.py

index 457b759ead39f61d9eb0863c9de527473658b91b..50bd6121ce5fac38beea77ef8a677b52c5b31468 100644 (file)
@@ -70,6 +70,16 @@ export class ServicesPageHelper extends PageHelper {
     }
   }
 
+  editService(name: string, count: string) {
+    this.navigateEdit(name, true, false);
+    cy.get(`${this.pages.create.id}`).within(() => {
+      cy.get('#service_type').should('be.disabled');
+      cy.get('#service_id').should('be.disabled');
+      cy.get('#count').clear().type(count);
+      cy.get('cd-submit-button').click();
+    });
+  }
+
   checkServiceStatus(daemon: string) {
     this.getTableCell(this.serviceDetailColumnIndex.daemonType, daemon)
       .parent()
@@ -80,6 +90,16 @@ export class ServicesPageHelper extends PageHelper {
       });
   }
 
+  expectPlacementCount(serviceName: string, expectedCount: string) {
+    this.getTableCell(this.columnIndex.service_name, serviceName)
+      .parent()
+      .find(`datatable-body-cell:nth-child(${this.columnIndex.placement})`)
+      .should(($ele) => {
+        const running = $ele.text().split(';');
+        expect(running).to.include(`count:${expectedCount}`);
+      });
+  }
+
   checkExist(serviceName: string, exist: boolean) {
     this.getTableCell(this.columnIndex.service_name, serviceName).should(($elements) => {
       const services = $elements.map((_, el) => el.textContent).get();
index 9f5e8ceda862b81501ba9a10cccbd6da8e67e03d..fb5e6ac8923aae825fae4f71c27f587255881224 100644 (file)
@@ -2,6 +2,7 @@ import { ServicesPageHelper } from '../cluster/services.po';
 
 describe('Services page', () => {
   const services = new ServicesPageHelper();
+  const serviceName = 'rgw.foo';
 
   beforeEach(() => {
     cy.login();
@@ -14,7 +15,13 @@ describe('Services page', () => {
       services.navigateTo('create');
       services.addService('rgw');
 
-      services.checkExist('rgw.foo', true);
+      services.checkExist(serviceName, true);
+    });
+
+    it('should edit a service', () => {
+      const count = '2';
+      services.editService(serviceName, count);
+      services.expectPlacementCount(serviceName, count);
     });
 
     it('should create and delete an ingress service', () => {
index 5060a22649b57028b7fff509c6c223cc6c81ca85..aec174a99cdba26205b131dd82c36b0a2f1cbcbb 100644 (file)
@@ -20,11 +20,19 @@ describe('Create cluster create services page', () => {
   });
 
   describe('when Orchestrator is available', () => {
+    const serviceName = 'rgw.foo';
+
     it('should create an rgw service', () => {
       cy.get('.btn.btn-accent').first().click({ force: true });
 
-      createClusterServicePage.addService('rgw', false, '3');
-      createClusterServicePage.checkExist('rgw.foo', true);
+      createClusterServicePage.addService('rgw', false, '2');
+      createClusterServicePage.checkExist(serviceName, true);
+    });
+
+    it('should edit a service', () => {
+      const count = '3';
+      createClusterServicePage.editService(serviceName, count);
+      createClusterServicePage.expectPlacementCount(serviceName, count);
     });
 
     it('should create and delete an ingress service', () => {
index 176bca5a14774fd89c83bfbfc6b37fe0fb8681a3..85a536f05a9a1da12add503278a50a81608aecab 100644 (file)
@@ -52,14 +52,16 @@ export abstract class PageHelper {
   /**
    * Navigates to the edit page
    */
-  navigateEdit(name: string, select = true) {
+  navigateEdit(name: string, select = true, breadcrumb = true) {
     if (select) {
       this.navigateTo();
       this.getFirstTableCell(name).click();
     }
     cy.contains('Creating...').should('not.exist');
     cy.contains('button', 'Edit').click();
-    this.expectBreadcrumbText('Edit');
+    if (breadcrumb) {
+      this.expectBreadcrumbText('Edit');
+    }
   }
 
   /**
index e1eb456ee8aa0e20f5c940729306b9f865acdc50..959cb376997029d950661a888af9c8ca039426fb 100644 (file)
@@ -139,6 +139,11 @@ const routes: Routes = [
             path: URLVerbs.CREATE,
             component: ServiceFormComponent,
             outlet: 'modal'
+          },
+          {
+            path: `${URLVerbs.EDIT}/:type/:name`,
+            component: ServiceFormComponent,
+            outlet: 'modal'
           }
         ]
       },
index 5394dc1d33b210b222bf6f91f429445cc2df9448..4a4bb109472f83303572e7da1b7f97375d6599ef 100644 (file)
@@ -65,7 +65,7 @@
           <cd-services [hasDetails]="false"
                        [hiddenServices]="['mon', 'mgr', 'crash', 'agent']"
                        [hiddenColumns]="['status.running', 'status.size', 'status.last_refresh']"
-                       [modal]="false"></cd-services>
+                       [routedModal]="false"></cd-services>
         </div>
         <div *ngSwitchCase="'4'"
              class="ml-5">
index 99c0903dacf49682cf3c9cd7c98c554aea22a677..5582d300ddac868077730438d00ad9739e139c58 100644 (file)
                 <option *ngIf="pools === null"
                         [ngValue]="null"
                         i18n>Loading...</option>
-                <option *ngIf="pools !== null && pools.length === 0"
+                <option *ngIf="pools && pools.length === 0"
                         [ngValue]="null"
                         i18n>-- No pools available --</option>
-                <option *ngIf="pools !== null && pools.length > 0"
+                <option *ngIf="pools && pools.length > 0"
                         [ngValue]="null"
                         i18n>-- Select a pool --</option>
                 <option *ngFor="let pool of pools"
index fd3bc8025dbe04b661d3d2826ca3acc35716518c..a1078b81ba2abd3da8bfb80b7ea5d31104d503ca 100644 (file)
@@ -433,5 +433,22 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
         formHelper.expectError('monitor_port', 'pattern');
       });
     });
+
+    describe('check edit fields', () => {
+      beforeEach(() => {
+        component.editing = true;
+      });
+
+      it('should check whether edit field is correctly loaded', () => {
+        const cephServiceSpy = spyOn(cephServiceService, 'list').and.callThrough();
+        component.ngOnInit();
+        expect(cephServiceSpy).toBeCalledTimes(2);
+        expect(component.action).toBe('Edit');
+        const serviceType = fixture.debugElement.query(By.css('#service_type')).nativeElement;
+        const serviceId = fixture.debugElement.query(By.css('#service_id')).nativeElement;
+        expect(serviceType.disabled).toBeTruthy();
+        expect(serviceId.disabled).toBeTruthy();
+      });
+    });
   });
 });
index da4daf9c1f5f4ad8bf1d99dfbb4b005c3eb2a8ec..6db898f0aa847cf4c350085703a031e913a84b10 100644 (file)
@@ -1,6 +1,6 @@
 import { Component, Input, OnInit, ViewChild } from '@angular/core';
 import { AbstractControl, Validators } from '@angular/forms';
-import { Router } from '@angular/router';
+import { ActivatedRoute, Router } from '@angular/router';
 
 import { NgbActiveModal, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
 import _ from 'lodash';
@@ -31,7 +31,13 @@ export class ServiceFormComponent extends CdForm implements OnInit {
   @ViewChild(NgbTypeahead, { static: false })
   typeahead: NgbTypeahead;
 
-  @Input() public hiddenServices: string[] = [];
+  @Input() hiddenServices: string[] = [];
+
+  @Input() editing = false;
+
+  @Input() serviceName: string;
+
+  @Input() serviceType: string;
 
   serviceForm: CdFormGroup;
   action: string;
@@ -53,6 +59,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
     private poolService: PoolService,
     private router: Router,
     private taskWrapperService: TaskWrapperService,
+    private route: ActivatedRoute,
     public activeModal: NgbActiveModal
   ) {
     super();
@@ -213,10 +220,17 @@ export class ServiceFormComponent extends CdForm implements OnInit {
   }
 
   ngOnInit(): void {
-    if (this.router.url.includes('services')) {
+    this.action = this.actionLabels.CREATE;
+    if (this.router.url.includes('services/(modal:create')) {
+      this.pageURL = 'services';
+    } else if (this.router.url.includes('services/(modal:edit')) {
+      this.editing = true;
       this.pageURL = 'services';
+      this.route.params.subscribe((params: { type: string; name: string }) => {
+        this.serviceName = params.name;
+        this.serviceType = params.type;
+      });
     }
-    this.action = this.actionLabels.CREATE;
     this.cephServiceService.getKnownTypes().subscribe((resp: Array<string>) => {
       // Remove service types:
       // osd       - This is deployed a different way.
@@ -244,6 +258,79 @@ export class ServiceFormComponent extends CdForm implements OnInit {
     this.cephServiceService.list().subscribe((services: CephServiceSpec[]) => {
       this.services = services.filter((service: any) => service.service_type === 'rgw');
     });
+
+    if (this.editing) {
+      this.action = this.actionLabels.EDIT;
+      this.disableForEditing(this.serviceType);
+      this.cephServiceService.list(this.serviceName).subscribe((response: CephServiceSpec[]) => {
+        const formKeys = ['service_type', 'service_id', 'unmanaged'];
+        formKeys.forEach((keys) => {
+          this.serviceForm.get(keys).setValue(response[0][keys]);
+        });
+        if (!response[0]['unmanaged']) {
+          const placementKey = Object.keys(response[0]['placement'])[0];
+          let placementValue: string;
+          ['hosts', 'label'].indexOf(placementKey) >= 0
+            ? (placementValue = placementKey)
+            : (placementValue = 'hosts');
+          this.serviceForm.get('placement').setValue(placementValue);
+          this.serviceForm.get('count').setValue(response[0]['placement']['count']);
+          if (response[0]?.placement[placementValue]) {
+            this.serviceForm.get(placementValue).setValue(response[0]?.placement[placementValue]);
+          }
+        }
+        switch (this.serviceType) {
+          case 'iscsi':
+            const specKeys = ['pool', 'api_password', 'api_user', 'trusted_ip_list', 'api_port'];
+            specKeys.forEach((key) => {
+              this.serviceForm.get(key).setValue(response[0].spec[key]);
+            });
+            this.serviceForm.get('ssl').setValue(response[0].spec?.api_secure);
+            if (response[0].spec?.api_secure) {
+              this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert);
+              this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key);
+            }
+            break;
+          case 'rgw':
+            this.serviceForm.get('rgw_frontend_port').setValue(response[0].spec?.rgw_frontend_port);
+            this.serviceForm.get('ssl').setValue(response[0].spec?.ssl);
+            if (response[0].spec?.ssl) {
+              this.serviceForm
+                .get('ssl_cert')
+                .setValue(response[0].spec?.rgw_frontend_ssl_certificate);
+            }
+            break;
+          case 'ingress':
+            const ingressSpecKeys = [
+              'backend_service',
+              'virtual_ip',
+              'frontend_port',
+              'monitor_port',
+              'virtual_interface_networks',
+              'ssl'
+            ];
+            ingressSpecKeys.forEach((key) => {
+              this.serviceForm.get(key).setValue(response[0].spec[key]);
+            });
+            if (response[0].spec?.ssl) {
+              this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert);
+              this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key);
+            }
+            break;
+        }
+      });
+    }
+  }
+
+  disableForEditing(serviceType: string) {
+    const disableForEditKeys = ['service_type', 'service_id'];
+    disableForEditKeys.forEach((key) => {
+      this.serviceForm.get(key).disable();
+    });
+    switch (serviceType) {
+      case 'ingress':
+        this.serviceForm.get('backend_service').disable();
+    }
   }
 
   searchLabels = (text$: Observable<string>) => {
@@ -282,8 +369,12 @@ export class ServiceFormComponent extends CdForm implements OnInit {
 
   onSubmit() {
     const self = this;
-    const values: object = this.serviceForm.value;
+    const values: object = this.serviceForm.getRawValue();
     const serviceType: string = values['service_type'];
+    let taskUrl = `service/${URLVerbs.CREATE}`;
+    if (this.editing) {
+      taskUrl = `service/${URLVerbs.EDIT}`;
+    }
     const serviceSpec: object = {
       service_type: serviceType,
       placement: {},
@@ -327,7 +418,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
           }
           serviceSpec['ssl'] = values['ssl'];
           if (values['ssl']) {
-            serviceSpec['rgw_frontend_ssl_certificate'] = values['ssl_cert'].trim();
+            serviceSpec['rgw_frontend_ssl_certificate'] = values['ssl_cert']?.trim();
           }
           break;
         case 'iscsi':
@@ -342,8 +433,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
           serviceSpec['api_password'] = values['api_password'];
           serviceSpec['api_secure'] = values['ssl'];
           if (values['ssl']) {
-            serviceSpec['ssl_cert'] = values['ssl_cert'].trim();
-            serviceSpec['ssl_key'] = values['ssl_key'].trim();
+            serviceSpec['ssl_cert'] = values['ssl_cert']?.trim();
+            serviceSpec['ssl_key'] = values['ssl_key']?.trim();
           }
           break;
         case 'ingress':
@@ -360,16 +451,17 @@ export class ServiceFormComponent extends CdForm implements OnInit {
           }
           serviceSpec['ssl'] = values['ssl'];
           if (values['ssl']) {
-            serviceSpec['ssl_cert'] = values['ssl_cert'].trim();
-            serviceSpec['ssl_key'] = values['ssl_key'].trim();
+            serviceSpec['ssl_cert'] = values['ssl_cert']?.trim();
+            serviceSpec['ssl_key'] = values['ssl_key']?.trim();
           }
           serviceSpec['virtual_interface_networks'] = values['virtual_interface_networks'];
           break;
       }
     }
+
     this.taskWrapperService
       .wrapTaskAroundCall({
-        task: new FinishedTask(`service/${URLVerbs.CREATE}`, {
+        task: new FinishedTask(taskUrl, {
           service_name: serviceName
         }),
         call: this.cephServiceService.create(serviceSpec)
index c2346c2747990aaa9938e18bdea790478c84dc15..318a54a6ee4589f892f65c327617a4b1e6f897cd 100644 (file)
@@ -49,7 +49,7 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
 
   @Input() hasDetails = true;
 
-  @Input() modal = true;
+  @Input() routedModal = true;
 
   permissions: Permissions;
   tableActions: CdTableAction[];
@@ -59,6 +59,7 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
   orchStatus: OrchestratorStatus;
   actionOrchFeatures = {
     create: [OrchestratorFeature.SERVICE_CREATE],
+    update: [OrchestratorFeature.SERVICE_EDIT],
     delete: [OrchestratorFeature.SERVICE_DELETE]
   };
 
@@ -88,6 +89,13 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
         canBePrimary: (selection: CdTableSelection) => !selection.hasSelection,
         disable: (selection: CdTableSelection) => this.getDisable('create', selection)
       },
+      {
+        permission: 'update',
+        icon: Icons.edit,
+        click: () => this.openModal(true),
+        name: this.actionLabels.EDIT,
+        disable: (selection: CdTableSelection) => this.getDisable('update', selection)
+      },
       {
         permission: 'delete',
         icon: Icons.destroy,
@@ -98,12 +106,36 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
     ];
   }
 
-  openModal() {
-    if (this.modal) {
-      this.router.navigate([BASE_URL, { outlets: { modal: [URLVerbs.CREATE] } }]);
+  openModal(edit = false) {
+    if (this.routedModal) {
+      edit
+        ? this.router.navigate([
+            BASE_URL,
+            {
+              outlets: {
+                modal: [
+                  URLVerbs.EDIT,
+                  this.selection.first().service_type,
+                  this.selection.first().service_name
+                ]
+              }
+            }
+          ])
+        : this.router.navigate([BASE_URL, { outlets: { modal: [URLVerbs.CREATE] } }]);
     } else {
-      this.bsModalRef = this.modalService.show(ServiceFormComponent);
-      this.bsModalRef.componentInstance.hiddenServices = this.hiddenServices;
+      let initialState = {};
+      edit
+        ? (initialState = {
+            serviceName: this.selection.first()?.service_name,
+            serviceType: this.selection?.first()?.service_type,
+            hiddenServices: this.hiddenServices,
+            editing: edit
+          })
+        : (initialState = {
+            hiddenServices: this.hiddenServices,
+            editing: edit
+          });
+      this.bsModalRef = this.modalService.show(ServiceFormComponent, initialState, { size: 'lg' });
     }
   }
 
@@ -155,12 +187,21 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
     }
   }
 
-  getDisable(action: 'create' | 'delete', selection: CdTableSelection): boolean | string {
+  getDisable(
+    action: 'create' | 'update' | 'delete',
+    selection: CdTableSelection
+  ): boolean | string {
     if (action === 'delete') {
       if (!selection?.hasSingleSelection) {
         return true;
       }
     }
+    if (action === 'update') {
+      const disableEditServices = ['osd', 'container'];
+      if (disableEditServices.indexOf(this.selection.first()?.service_type) >= 0) {
+        return true;
+      }
+    }
     return this.orchService.getTableActionDisableDesc(
       this.orchStatus,
       this.actionOrchFeatures[action]
index 545d072fc45f415cc48a9d1f4e59d58af56950de..077db0855a82b444e70d2464ca8d51803a47b2c9 100644 (file)
@@ -10,6 +10,7 @@ export enum OrchestratorFeature {
 
   SERVICE_LIST = 'describe_service',
   SERVICE_CREATE = 'apply',
+  SERVICE_EDIT = 'apply',
   SERVICE_DELETE = 'remove_service',
   SERVICE_RELOAD = 'service_action',
   DAEMON_LIST = 'list_daemons',
index 151e0bd2987cfdaf74f497fbab412223acacf0d4..dd64422e10f40d83afe57bc38551e517a40a1ee4 100644 (file)
@@ -14,4 +14,32 @@ export interface CephServiceSpec {
   service_id: string;
   unmanaged: boolean;
   status: CephServiceStatus;
+  spec: CephServiceAdditionalSpec;
+  placement: CephServicePlacement;
+}
+
+export interface CephServiceAdditionalSpec {
+  backend_service: string;
+  api_user: string;
+  api_password: string;
+  api_port: number;
+  api_secure: boolean;
+  rgw_frontend_port: number;
+  trusted_ip_list: string[];
+  virtual_ip: string;
+  frontend_port: number;
+  monitor_port: number;
+  virtual_interface_networks: string[];
+  pool: string;
+  rgw_frontend_ssl_certificate: string;
+  ssl: boolean;
+  ssl_cert: string;
+  ssl_key: string;
+}
+
+export interface CephServicePlacement {
+  count: number;
+  placement: string;
+  hosts: string[];
+  label: string;
 }
index 8705f29991665442308c31fb6378cdbaa6bd43a8..5adabe2115370d9c65ab4232a7325e0c343bc594 100644 (file)
@@ -334,6 +334,9 @@ export class TaskMessageService {
     'service/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
       this.service(metadata)
     ),
+    'service/edit': this.newTaskMessage(this.commonOperations.update, (metadata) =>
+      this.service(metadata)
+    ),
     'service/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
       this.service(metadata)
     )
index 671ebdb751a39269a6ced199ff7b6a49503fd0a5..05dd2a21a01ade39073b12ba3593de51a75f2a8b 100644 (file)
@@ -201,6 +201,7 @@ class OrchFeature(object):
 
     SERVICE_LIST = 'describe_service'
     SERVICE_CREATE = 'apply'
+    SERVICE_EDIT = 'apply'
     SERVICE_DELETE = 'remove_service'
     SERVICE_RELOAD = 'service_action'
     DAEMON_LIST = 'list_daemons'