From: Dnyaneshwari Date: Wed, 12 Feb 2025 13:30:29 +0000 (+0530) Subject: mgr/dashboard: SMB - Edit Cluster X-Git-Tag: testing/wip-pdonnell-testing-20250227.000317-debug~4^2 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=67659230bf84501c95d958559eab6d9b9686eae1;p=ceph-ci.git mgr/dashboard: SMB - Edit Cluster Fixes: https://tracker.ceph.com/issues/69964 Signed-off-by: Dnyaneshwari Talwekar --- diff --git a/src/pybind/mgr/dashboard/controllers/smb.py b/src/pybind/mgr/dashboard/controllers/smb.py index bc9323e9947..88f471c76eb 100644 --- a/src/pybind/mgr/dashboard/controllers/smb.py +++ b/src/pybind/mgr/dashboard/controllers/smb.py @@ -36,6 +36,10 @@ CLUSTER_SCHEMA = { "ref": (str, "Reference identifier for the user group resource") }], "User group settings for user auth mode"), "custom_dns": ([str], "List of custom DNS server addresses"), + "public_addrs": ([{ + "address": (str, "This address will be assigned to one of the host's network devices"), + "destination": (str, "Defines where the system will assign the managed IPs.") + }], "Public Address"), "placement": ({ "count": (int, "Number of instances to place") }, "Placement configuration for the resource") diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 283d9e8f1a5..b9b9d453a05 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -460,6 +460,11 @@ const routes: Routes = [ component: SmbShareFormComponent, data: { breadcrumbs: ActionLabels.CREATE } }, + { + path: `${URLVerbs.EDIT}/:cluster_id`, + component: SmbClusterFormComponent, + data: { breadcrumbs: ActionLabels.EDIT } + }, { path: `ad/${URLVerbs.CREATE}`, component: SmbJoinAuthFormComponent, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.html index d21972ce0ec..e0db02f9270 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.html @@ -1,407 +1,303 @@
-
-
- {{ action | titlecase }} {{ resource | upperFirst }} -
- - -
- Cluster Name - - - - This field is required. - -
+ + +
+ {{ action | titlecase }} {{ resource | upperFirst }} +
- -
- - - - - - +
+ This field is required. - -
- - -
-
- Active Directory (AD) Settings -
- - - - - - - - -
+ helperText="Unique identifier" + i18n-helperText + cdRequiredField="Cluster Name" + [disabled]="isEdit" + [invalid]="smbForm.controls.cluster_id.invalid && smbForm.controls.cluster_id.dirty" + [invalidText]="clusterError" + >Cluster Name +
-
- Specify the Realm and AD access resources in the Domain Settings field. -
-
- - - -
-
- - - - - - This field is required. - -
-
- + This field is required. - - -
+
-
- -
- - - -
- - +
- - + + -
- - -
- + This field is required. - - - - This field is required. - -
- - -
- - - -
-
-
- -
- -
+
+
- - -
+ +
- DNS - + [columnNumbers]="{ md: 12 }" + class="d-flex"> + Active Directory (AD) Settings +
+ + + + + + + + +
+ Specify the Realm and AD access resources in the Domain Settings field. +
+
+ + +
- + + + + + + This field is required. + +
+
- - - + + +
-
- + + +
+ -
- - One or more IP Addresses that will be - applied to the Samba containers to override - the default DNS resolver(s). This option is - intended to be used when the host Ceph node - is not configured to resolve DNS entries within - AD domain(s). - -
- - -
- - - -
+ class="cds--btn__icon"> + +
- - - -
- -
- + +
+ + + + +
+ + +
+ Address - - - + > + + + This field is required. - This field is required. -
- + + +
+ + + +
+
+
+ +
+ +
+ + + +
+ [columnNumbers]="{ lg: 14 }"> Destination - + >DNS +
- + +
- -
- - Assign virtual IP addresses that will be managed - by the clustering subsystem and may automatically - move between nodes running Samba containers. -
- - + +
+ + One or more IP Addresses that will be + applied to the Samba containers to override + the default DNS resolver(s). This option is + intended to be used when the host Ceph node + is not configured to resolve DNS entries within + AD domain(s). + +
+ + +
+ + + +
+ + + + +
+ +
+ Address + + + + + This field is required. + + +
+ +
+ Destination + + +
+
+ + + +
+
+
+
+
+ + Assign virtual IP addresses that will be managed + by the clustering subsystem and may automatically + move between nodes running Samba containers. +
+ + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts index cbbcd4abca1..e48619c5824 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { forkJoin, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -14,7 +14,8 @@ import { CLUSTER_RESOURCE, ClusterRequestModel, SMBUsersGroups, - PublicAddress + PublicAddress, + SMBCluster } from '../smb.model'; import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; import { Icons } from '~/app/shared/enum/icons.enum'; @@ -34,6 +35,7 @@ import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { SmbDomainSettingModalComponent } from '../smb-domain-setting-modal/smb-domain-setting-modal.component'; import { CephServicePlacement } from '~/app/shared/models/service.interface'; import { USERSGROUPS_URL } from '../smb-usersgroups-list/smb-usersgroups-list.component'; +import { UpperFirstPipe } from '~/app/shared/pipes/upper-first.pipe'; @Component({ selector: 'cd-smb-cluster-form', @@ -53,6 +55,9 @@ export class SmbClusterFormComponent extends CdForm implements OnInit { resource: string; icons = Icons; domainSettingsObject: DomainSettings; + isEdit = false; + cluster_id: string; + clusterResponse: SMBCluster; modalData$!: Observable; usersGroups$: Observable; @@ -65,20 +70,25 @@ export class SmbClusterFormComponent extends CdForm implements OnInit { private modalService: ModalCdsService, private taskWrapperService: TaskWrapperService, private router: Router, - private cd: ChangeDetectorRef + private cd: ChangeDetectorRef, + private route: ActivatedRoute ) { super(); + this.resource = $localize`Cluster`; this.modalData$ = this.smbService.modalData$; } + ngOnInit() { this.action = this.actionLabels.CREATE; this.usersGroups$ = this.smbService.listUsersGroups(); + if (this.router.url.startsWith(`/cephfs/smb/${URLVerbs.EDIT}`)) { + this.isEdit = true; + } this.smbService.modalData$.subscribe((data: DomainSettings) => { this.domainSettingsObject = data; this.smbForm.get('domain_settings').setValue(data?.realm); }); - this.createForm(); this.hostsAndLabels$ = forkJoin({ hosts: this.hostService.getAllHosts(), labels: this.hostService.getLabels() @@ -88,9 +98,85 @@ export class SmbClusterFormComponent extends CdForm implements OnInit { labels: labels.map((label: string) => ({ content: label })) })) ); + + this.createForm(); + if (this.isEdit) { + this.action = this.actionLabels.EDIT; + this.smbForm.get('cluster_id').disable(); + this.smbForm.get('auth_mode').disable(); + this.route.params.subscribe((params: { cluster_id: string }) => { + this.cluster_id = params.cluster_id; + }); + + this.smbService.getCluster(this.cluster_id).subscribe((res: SMBCluster) => { + this.clusterResponse = res; + + const customDnsList = this.clusterResponse.custom_dns; + const customDnsFormArray = this.smbForm.get('custom_dns') as FormArray; + const joinSourcesArray = this.smbForm.get('joinSources') as FormArray; + const pubAddresses = this.clusterResponse.public_addrs; + const publicAddrsFormArray = this.smbForm.get('public_addrs') as FormArray; + + if (this.clusterResponse.clustering) { + const responseClustering = this.clusterResponse.clustering; + const upperFirstPipe = new UpperFirstPipe(); + const upperCaseCluster = upperFirstPipe.transform(responseClustering); + this.smbForm.get('clustering').setValue(upperCaseCluster || ''); + } + if (customDnsList?.length) { + customDnsList.forEach((dns: string) => { + customDnsFormArray.push(new FormControl(dns)); + }); + } + if (this.clusterResponse.auth_mode == AUTHMODE.activeDirectory) { + this.domainSettingsObject = this.clusterResponse?.domain_settings; + this.smbForm.get('domain_settings').setValue(this.domainSettingsObject.realm); + } else { + if ( + this.clusterResponse.user_group_settings && + this.clusterResponse.user_group_settings.length > 0 + ) { + this.clusterResponse.user_group_settings.forEach((JoinSource: JoinSource) => { + joinSourcesArray.push(new FormControl(JoinSource.ref)); + }); + const joinSourceRef = this.clusterResponse.user_group_settings.map( + (JoinSource: JoinSource) => JoinSource.ref + ); + joinSourcesArray.setValue(joinSourceRef); + } + } + this.smbForm.get('cluster_id').setValue(this.clusterResponse.cluster_id); + this.smbForm.get('auth_mode').setValue(this.clusterResponse.auth_mode); + if (this.clusterResponse.placement.count) { + this.smbForm.get('count').setValue(this.clusterResponse.placement.count); + } + if (pubAddresses?.length) { + pubAddresses.forEach((pubAddress: PublicAddress) => { + publicAddrsFormArray.push( + this.formBuilder.group({ + address: [pubAddress.address, Validators.required], + destination: [pubAddress.destination || ''] + }) + ); + }); + } + }); + } else { + this.action = this.actionLabels.CREATE; + this.hostsAndLabels$ = forkJoin({ + hosts: this.hostService.getAllHosts(), + labels: this.hostService.getLabels() + }).pipe( + map(({ hosts, labels }) => ({ + hosts: hosts.map((host: any) => ({ content: host['hostname'] })), + labels: labels.map((label: string) => ({ content: label })) + })) + ); + } this.orchStatus$ = this.orchService.status(); this.allClustering = Object.values(CLUSTERING); - this.onAuthModeChange(); + this.loadingReady(); + if (!this.isEdit) this.onAuthModeChange(); } createForm() { @@ -157,9 +243,17 @@ export class SmbClusterFormComponent extends CdForm implements OnInit { } else if (authMode === AUTHMODE.User) { const control = new FormControl(null, Validators.required); userGroupSettingsControl.push(control); + if (domainSettingsControl) { + domainSettingsControl.setValue(''); + this.domainSettingsObject = { realm: '', join_sources: [] }; + } domainSettingsControl.setErrors(null); - domainSettingsControl.clearValidators(); userGroupSettingsControl.setValidators(Validators.required); + + if (domainSettingsControl) { + domainSettingsControl.clearValidators(); + domainSettingsControl.updateValueAndValidity(); + } } else { if (userGroupSettingsControl) { userGroupSettingsControl.clearValidators(); @@ -173,27 +267,50 @@ export class SmbClusterFormComponent extends CdForm implements OnInit { const domainSettingsControl = this.smbForm.get('domain_settings'); const authMode = this.smbForm.get('auth_mode').value; + const values = this.smbForm.getRawValue(); + const serviceSpec: object = { + placement: {} + }; + switch (values['placement']) { + case PLACEMENT.host: + if (values['hosts'].length > 0) { + serviceSpec['placement']['hosts'] = this.selectedHosts; + } + break; + case PLACEMENT.label: + serviceSpec['placement']['label'] = this.selectedLabels; + break; + } + // Domain Setting should be mandatory if authMode is "Active Directory" if (authMode === AUTHMODE.activeDirectory && !domainSettingsControl.value) { domainSettingsControl.setErrors({ required: true }); this.smbForm.markAllAsTouched(); return; } - const component = this; + if (this.isEdit) { + this.handleTaskRequest(URLVerbs.EDIT); + } else { + this.handleTaskRequest(URLVerbs.CREATE); + } + } + + handleTaskRequest(urlVerb: string) { const requestModel = this.buildRequest(); const BASE_URL = 'smb/cluster'; + const component = this; const cluster_id = this.smbForm.get('cluster_id').value; - const taskUrl = `${BASE_URL}/${URLVerbs.CREATE}`; + this.taskWrapperService .wrapTaskAroundCall({ - task: new FinishedTask(taskUrl, { cluster_id }), + task: new FinishedTask(`${BASE_URL}/${urlVerb}`, { cluster_id }), call: this.smbService.createCluster(requestModel) }) .subscribe({ complete: () => { this.router.navigate([`cephfs/smb`]); }, - error() { + error: () => { component.smbForm.setErrors({ cdSubmitButton: true }); } }); @@ -202,15 +319,18 @@ export class SmbClusterFormComponent extends CdForm implements OnInit { private buildRequest() { const values = this.smbForm.getRawValue(); const rawFormValue = _.cloneDeep(this.smbForm.value); + const clusterId = this.smbForm.get('cluster_id')?.value; + const authMode = this.smbForm.get('auth_mode')?.value; + const joinSources: JoinSource[] = (this.domainSettingsObject?.join_sources || []) .filter((source: { ref: string }) => source.ref) .map((source: { ref: string }) => ({ ref: source.ref, - source_type: RESOURCE.Resource + sourceType: RESOURCE.Resource })); const joinSourceObj = joinSources.map((source: JoinSource) => ({ - source_type: RESOURCE.Resource, + sourceType: RESOURCE.Resource, ref: source.ref })); @@ -222,8 +342,8 @@ export class SmbClusterFormComponent extends CdForm implements OnInit { const requestModel: ClusterRequestModel = { cluster_resource: { resource_type: CLUSTER_RESOURCE, - cluster_id: rawFormValue.cluster_id, - auth_mode: rawFormValue.auth_mode + cluster_id: clusterId, + auth_mode: authMode } }; @@ -260,6 +380,10 @@ export class SmbClusterFormComponent extends CdForm implements OnInit { requestModel.cluster_resource.clustering = rawFormValue.clustering.toLowerCase(); } + if (rawFormValue.placement.count) { + requestModel.cluster_resource.count = rawFormValue.placement.count; + } + return requestModel; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.ts index cdbd2a5f9e2..d7a6dfcc687 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.ts @@ -14,11 +14,11 @@ import { Permission } from '~/app/shared/models/permissions'; import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; import { SmbService } from '~/app/shared/api/smb.service'; - +import { SMBCluster } from '../smb.model'; import { Icons } from '~/app/shared/enum/icons.enum'; import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { URLBuilderService } from '~/app/shared/services/url-builder.service'; -import { SMBCluster } from '../smb.model'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; @@ -38,9 +38,10 @@ export class SmbClusterListComponent extends ListWithDetails implements OnInit { permission: Permission; tableActions: CdTableAction[]; context: CdTableFetchDataContext; - selection = new CdTableSelection(); smbClusters$: Observable; subject$ = new BehaviorSubject([]); + selection = new CdTableSelection(); + modalRef: NgbModalRef; constructor( private authStorageService: AuthStorageService, @@ -73,9 +74,15 @@ export class SmbClusterListComponent extends ListWithDetails implements OnInit { permission: 'create', icon: Icons.add, routerLink: () => this.urlBuilder.getCreate(), - canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection }, + { + name: this.actionLabels.EDIT, + permission: 'update', + icon: Icons.edit, + routerLink: () => + this.selection.first() && this.urlBuilder.getEdit(this.selection.first().cluster_id) + }, { permission: 'delete', icon: Icons.destroy, @@ -95,7 +102,6 @@ export class SmbClusterListComponent extends ListWithDetails implements OnInit { ) ); } - loadSMBCluster() { this.subject$.next([]); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts index 58eb555b55a..83101fdbb78 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts @@ -8,8 +8,9 @@ export interface SMBCluster { user_group_settings?: JoinSource[]; custom_dns?: string[]; placement?: CephServicePlacement; - clustering?: typeof CLUSTERING; + clustering?: Clustering; public_addrs?: PublicAddress[]; + count?: number; } export interface ClusterRequestModel { @@ -45,7 +46,7 @@ export interface DomainSettings { } export interface JoinSource { - source_type?: string; + sourceType: string; ref: string; } @@ -143,3 +144,5 @@ export const USERSGROUPS_RESOURCE = 'ceph.smb.usersgroups' as const; export const PROVIDER = 'samba-vfs'; export const SHARE_URL = '/cephfs/smb/share/'; + +type Clustering = 'default' | 'never' | 'always'; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts index a263a11b94d..16e06fdf409 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts @@ -40,11 +40,12 @@ describe('SmbService', () => { resource_type: CLUSTER_RESOURCE, cluster_id: 'clusterUserTest', auth_mode: 'active-directory', + intent: 'present', domain_settings: { realm: 'DOMAIN1.SINK.TEST', join_sources: [ { - source_type: 'resource', + sourceType: 'resource', ref: 'join1-admin' } ] diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts index 960886ef55c..bf4c64a37b0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts @@ -97,4 +97,8 @@ export class SmbService { observe: 'response' }); } + + getCluster(cluster_id: string) { + return this.http.get(`${this.baseURL}/cluster/${cluster_id}`); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts index 6fb5a114821..27c469fe647 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts @@ -528,6 +528,9 @@ export class TaskMessageService { ), 'cephfs/smb/standalone/create': this.newTaskMessage(this.commonOperations.create, (metadata) => this.smbUsersgroups(metadata) + ), + 'smb/cluster/edit': this.newTaskMessage(this.commonOperations.update, (metadata) => + this.smbCluster(metadata) ) }; diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 2033da0c1de..73af08d45bb 100755 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -15036,6 +15036,23 @@ paths: required: - count type: object + public_addrs: + description: Public Address + items: + properties: + address: + description: This address will be assigned to one of the + host's network devices + type: string + destination: + description: Defines where the system will assign the + managed IPs. + type: string + required: + - address + - destination + type: object + type: array resource_type: description: ceph.smb.cluster type: string @@ -15063,6 +15080,7 @@ paths: - domain_settings - user_group_settings - custom_dns + - public_addrs - placement type: array description: OK @@ -15161,6 +15179,23 @@ paths: required: - count type: object + public_addrs: + description: Public Address + items: + properties: + address: + description: This address will be assigned to + one of the host's network devices + type: string + destination: + description: Defines where the system will assign + the managed IPs. + type: string + required: + - address + - destination + type: object + type: array resource_type: description: ceph.smb.cluster type: string @@ -15188,6 +15223,7 @@ paths: - domain_settings - user_group_settings - custom_dns + - public_addrs - placement type: object state: @@ -15334,6 +15370,23 @@ paths: required: - count type: object + public_addrs: + description: Public Address + items: + properties: + address: + description: This address will be assigned to one of the + host's network devices + type: string + destination: + description: Defines where the system will assign the managed + IPs. + type: string + required: + - address + - destination + type: object + type: array resource_type: description: ceph.smb.cluster type: string @@ -15360,6 +15413,7 @@ paths: - domain_settings - user_group_settings - custom_dns + - public_addrs - placement type: object description: OK