]> git.apps.os.sepia.ceph.com Git - ceph.git/blob
878ab179b188af42a2909ce95e48e5b6054031c3
[ceph.git] /
1 import { HttpParams } from '@angular/common/http';
2 import { Component, Input, OnInit, ViewChild } from '@angular/core';
3 import { AbstractControl, UntypedFormControl, Validators } from '@angular/forms';
4 import { ActivatedRoute, Router } from '@angular/router';
5
6 import { NgbActiveModal, NgbModalRef, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
7 import _ from 'lodash';
8 import { forkJoin, merge, Observable, Subject, Subscription } from 'rxjs';
9 import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators';
10 import { Pool } from '~/app/ceph/pool/pool';
11 import { CreateRgwServiceEntitiesComponent } from '~/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component';
12 import { RgwRealm, RgwZonegroup, RgwZone } from '~/app/ceph/rgw/models/rgw-multisite';
13
14 import { CephServiceService } from '~/app/shared/api/ceph-service.service';
15 import { HostService } from '~/app/shared/api/host.service';
16 import { PoolService } from '~/app/shared/api/pool.service';
17 import { RbdService } from '~/app/shared/api/rbd.service';
18 import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
19 import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
20 import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
21 import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
22 import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
23 import { SelectOption } from '~/app/shared/components/select/select-option.model';
24 import {
25   ActionLabelsI18n,
26   TimerServiceInterval,
27   URLVerbs
28 } from '~/app/shared/constants/app.constants';
29 import { CdForm } from '~/app/shared/forms/cd-form';
30 import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
31 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
32 import { CdValidators } from '~/app/shared/forms/cd-validators';
33 import { FinishedTask } from '~/app/shared/models/finished-task';
34 import { CephServiceSpec } from '~/app/shared/models/service.interface';
35 import { ModalService } from '~/app/shared/services/modal.service';
36 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
37 import { TimerService } from '~/app/shared/services/timer.service';
38
39 @Component({
40   selector: 'cd-service-form',
41   templateUrl: './service-form.component.html',
42   styleUrls: ['./service-form.component.scss']
43 })
44 export class ServiceFormComponent extends CdForm implements OnInit {
45   public sub = new Subscription();
46
47   readonly MDS_SVC_ID_PATTERN = /^[a-zA-Z_.-][a-zA-Z0-9_.-]*$/;
48   readonly SNMP_DESTINATION_PATTERN = /^[^\:]+:[0-9]/;
49   readonly SNMP_ENGINE_ID_PATTERN = /^[0-9A-Fa-f]{10,64}/g;
50   readonly INGRESS_SUPPORTED_SERVICE_TYPES = ['rgw', 'nfs'];
51   readonly SMB_CONFIG_URI_PATTERN = /^(http:|https:|rados:|rados:mon-config-key:)/;
52   readonly OAUTH2_ISSUER_URL_PATTERN = /^(https?:\/\/)?([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+)(:[0-9]{1,5})?(\/.*)?$/;
53   @ViewChild(NgbTypeahead, { static: false })
54   typeahead: NgbTypeahead;
55
56   @Input() hiddenServices: string[] = [];
57
58   @Input() editing = false;
59
60   @Input() serviceName: string;
61
62   @Input() serviceType: string;
63
64   serviceForm: CdFormGroup;
65   action: string;
66   resource: string;
67   serviceTypes: string[] = [];
68   serviceIds: string[] = [];
69   hosts: any;
70   labels: string[];
71   labelClick = new Subject<string>();
72   labelFocus = new Subject<string>();
73   pools: Array<Pool>;
74   rbdPools: Array<Pool>;
75   services: Array<CephServiceSpec> = [];
76   pageURL: string;
77   serviceList: CephServiceSpec[];
78   multisiteInfo: object[] = [];
79   defaultRealmId = '';
80   defaultZonegroupId = '';
81   defaultZoneId = '';
82   realmList: RgwRealm[] = [];
83   zonegroupList: RgwZonegroup[] = [];
84   zoneList: RgwZone[] = [];
85   bsModalRef: NgbModalRef;
86   defaultZonegroup: RgwZonegroup;
87   showRealmCreationForm = false;
88   defaultsInfo: { defaultRealmName: string; defaultZonegroupName: string; defaultZoneName: string };
89   realmNames: string[];
90   zonegroupNames: string[];
91   zoneNames: string[];
92   smbFeaturesList = ['domain'];
93
94   constructor(
95     public actionLabels: ActionLabelsI18n,
96     private cephServiceService: CephServiceService,
97     private formBuilder: CdFormBuilder,
98     private hostService: HostService,
99     private poolService: PoolService,
100     private rbdService: RbdService,
101     private router: Router,
102     private taskWrapperService: TaskWrapperService,
103     public timerService: TimerService,
104     public timerServiceVariable: TimerServiceInterval,
105     public rgwRealmService: RgwRealmService,
106     public rgwZonegroupService: RgwZonegroupService,
107     public rgwZoneService: RgwZoneService,
108     public rgwMultisiteService: RgwMultisiteService,
109     private route: ActivatedRoute,
110     public activeModal: NgbActiveModal,
111     public modalService: ModalService
112   ) {
113     super();
114     this.resource = $localize`service`;
115     this.hosts = {
116       options: [],
117       messages: new SelectMessages({
118         empty: $localize`There are no hosts.`,
119         filter: $localize`Filter hosts`
120       })
121     };
122     this.createForm();
123   }
124
125   createForm() {
126     this.serviceForm = this.formBuilder.group({
127       // Global
128       service_type: [null, [Validators.required]],
129       service_id: [
130         null,
131         [
132           CdValidators.composeIf(
133             {
134               service_type: 'mds'
135             },
136             [
137               Validators.required,
138               CdValidators.custom('mdsPattern', (value: string) => {
139                 if (_.isEmpty(value)) {
140                   return false;
141                 }
142                 return !this.MDS_SVC_ID_PATTERN.test(value);
143               })
144             ]
145           ),
146           CdValidators.requiredIf({
147             service_type: 'nfs'
148           }),
149           CdValidators.requiredIf({
150             service_type: 'iscsi'
151           }),
152           CdValidators.requiredIf({
153             service_type: 'nvmeof'
154           }),
155           CdValidators.requiredIf({
156             service_type: 'ingress'
157           }),
158           CdValidators.requiredIf({
159             service_type: 'smb'
160           }),
161           CdValidators.composeIf(
162             {
163               service_type: 'rgw'
164             },
165             [Validators.required]
166           ),
167           CdValidators.custom('uniqueName', (service_id: string) => {
168             return this.serviceIds && this.serviceIds.includes(service_id);
169           })
170         ]
171       ],
172       placement: ['hosts'],
173       label: [
174         null,
175         [
176           CdValidators.requiredIf({
177             placement: 'label',
178             unmanaged: false
179           })
180         ]
181       ],
182       hosts: [[]],
183       count: [null, [CdValidators.number(false)]],
184       unmanaged: [false],
185       // iSCSI
186       // NVMe/TCP
187       pool: [
188         null,
189         [
190           CdValidators.requiredIf({
191             service_type: 'iscsi'
192           }),
193           CdValidators.requiredIf({
194             service_type: 'nvmeof'
195           })
196         ]
197       ],
198       group: [
199         null,
200         CdValidators.requiredIf({
201           service_type: 'nvmeof'
202         })
203       ],
204       // RGW
205       rgw_frontend_port: [null, [CdValidators.number(false)]],
206       realm_name: [null],
207       zonegroup_name: [null],
208       zone_name: [null],
209       // iSCSI
210       trusted_ip_list: [null],
211       api_port: [null, [CdValidators.number(false)]],
212       api_user: [
213         null,
214         [
215           CdValidators.requiredIf({
216             service_type: 'iscsi',
217             unmanaged: false
218           })
219         ]
220       ],
221       api_password: [
222         null,
223         [
224           CdValidators.requiredIf({
225             service_type: 'iscsi',
226             unmanaged: false
227           })
228         ]
229       ],
230       // smb
231       cluster_id: [
232         null,
233         [
234           CdValidators.requiredIf({
235             service_type: 'smb'
236           })
237         ]
238       ],
239       features: new CdFormGroup(
240         this.smbFeaturesList.reduce((acc: object, e) => {
241           acc[e] = new UntypedFormControl(false);
242           return acc;
243         }, {})
244       ),
245       config_uri: [
246         null,
247         [
248           CdValidators.composeIf(
249             {
250               service_type: 'smb'
251             },
252             [
253               Validators.required,
254               CdValidators.custom('configUriPattern', (value: string) => {
255                 if (_.isEmpty(value)) {
256                   return false;
257                 }
258                 return !this.SMB_CONFIG_URI_PATTERN.test(value);
259               })
260             ]
261           )
262         ]
263       ],
264       custom_dns: [null],
265       join_sources: [null],
266       user_sources: [null],
267       include_ceph_users: [null],
268       // Ingress
269       backend_service: [
270         null,
271         [
272           CdValidators.requiredIf({
273             service_type: 'ingress'
274           })
275         ]
276       ],
277       virtual_ip: [
278         null,
279         [
280           CdValidators.requiredIf({
281             service_type: 'ingress'
282           })
283         ]
284       ],
285       frontend_port: [
286         null,
287         [
288           CdValidators.number(false),
289           CdValidators.requiredIf({
290             service_type: 'ingress'
291           })
292         ]
293       ],
294       monitor_port: [
295         null,
296         [
297           CdValidators.number(false),
298           CdValidators.requiredIf({
299             service_type: 'ingress'
300           })
301         ]
302       ],
303       virtual_interface_networks: [null],
304       // RGW, Ingress & iSCSI
305       ssl: [false],
306       ssl_cert: [
307         '',
308         [
309           CdValidators.composeIf(
310             {
311               service_type: 'rgw',
312               unmanaged: false,
313               ssl: true
314             },
315             [Validators.required, CdValidators.pemCert()]
316           ),
317           CdValidators.composeIf(
318             {
319               service_type: 'iscsi',
320               unmanaged: false,
321               ssl: true
322             },
323             [Validators.required, CdValidators.sslCert()]
324           ),
325           CdValidators.composeIf(
326             {
327               service_type: 'ingress',
328               unmanaged: false,
329               ssl: true
330             },
331             [Validators.required, CdValidators.pemCert()]
332           ),
333           CdValidators.composeIf(
334             {
335               service_type: 'oauth2-proxy',
336               unmanaged: false,
337               ssl: true
338             },
339             [Validators.required, CdValidators.sslCert()]
340           )
341         ]
342       ],
343       ssl_key: [
344         '',
345         [
346           CdValidators.composeIf(
347             {
348               service_type: 'iscsi',
349               unmanaged: false,
350               ssl: true
351             },
352             [Validators.required, CdValidators.sslPrivKey()]
353           ),
354           CdValidators.composeIf(
355             {
356               service_type: 'oauth2-proxy',
357               unmanaged: false,
358               ssl: true
359             },
360             [Validators.required, CdValidators.sslPrivKey()]
361           )
362         ]
363       ],
364       // snmp-gateway
365       snmp_version: [
366         null,
367         [
368           CdValidators.requiredIf({
369             service_type: 'snmp-gateway'
370           })
371         ]
372       ],
373       snmp_destination: [
374         null,
375         {
376           validators: [
377             CdValidators.requiredIf({
378               service_type: 'snmp-gateway'
379             }),
380             CdValidators.custom('snmpDestinationPattern', (value: string) => {
381               if (_.isEmpty(value)) {
382                 return false;
383               }
384               return !this.SNMP_DESTINATION_PATTERN.test(value);
385             })
386           ]
387         }
388       ],
389       engine_id: [
390         null,
391         [
392           CdValidators.requiredIf({
393             service_type: 'snmp-gateway'
394           }),
395           CdValidators.custom('snmpEngineIdPattern', (value: string) => {
396             if (_.isEmpty(value)) {
397               return false;
398             }
399             return !this.SNMP_ENGINE_ID_PATTERN.test(value);
400           })
401         ]
402       ],
403       auth_protocol: [
404         'SHA',
405         [
406           CdValidators.requiredIf({
407             service_type: 'snmp-gateway'
408           })
409         ]
410       ],
411       privacy_protocol: [null],
412       snmp_community: [
413         null,
414         [
415           CdValidators.requiredIf({
416             snmp_version: 'V2c'
417           })
418         ]
419       ],
420       snmp_v3_auth_username: [
421         null,
422         [
423           CdValidators.requiredIf({
424             service_type: 'snmp-gateway'
425           })
426         ]
427       ],
428       snmp_v3_auth_password: [
429         null,
430         [
431           CdValidators.requiredIf({
432             service_type: 'snmp-gateway'
433           })
434         ]
435       ],
436       snmp_v3_priv_password: [
437         null,
438         [
439           CdValidators.requiredIf({
440             privacy_protocol: { op: '!empty' }
441           })
442         ]
443       ],
444       grafana_port: [null, [CdValidators.number(false)]],
445       grafana_admin_password: [null],
446       // oauth2-proxy
447       provider_display_name: [
448         'My OIDC provider',
449         [
450           CdValidators.requiredIf({
451             service_type: 'oauth2-proxy'
452           })
453         ]
454       ],
455       client_id: [
456         null,
457         [
458           CdValidators.requiredIf({
459             service_type: 'oauth2-proxy'
460           })
461         ]
462       ],
463       client_secret: [
464         null,
465         [
466           CdValidators.requiredIf({
467             service_type: 'oauth2-proxy'
468           })
469         ]
470       ],
471       oidc_issuer_url: [
472         null,
473         [
474           CdValidators.requiredIf({
475             service_type: 'oauth2-proxy'
476           }),
477           CdValidators.custom('validUrl', (url: string) => {
478             if (_.isEmpty(url)) {
479               return false;
480             }
481             return !this.OAUTH2_ISSUER_URL_PATTERN.test(url);
482           })
483         ]
484       ],
485       https_address: [null, [CdValidators.oauthAddressTest()]],
486       redirect_url: [null],
487       allowlist_domains: [null]
488     });
489   }
490
491   resolveRoute() {
492     if (this.router.url.includes('services/(modal:create')) {
493       this.pageURL = 'services';
494       this.route.params.subscribe((params: { type: string }) => {
495         if (params?.type) {
496           this.serviceType = params.type;
497           this.serviceForm.get('service_type').setValue(this.serviceType);
498         }
499       });
500     } else if (this.router.url.includes('services/(modal:edit')) {
501       this.editing = true;
502       this.pageURL = 'services';
503       this.route.params.subscribe((params: { type: string; name: string }) => {
504         this.serviceName = params.name;
505         this.serviceType = params.type;
506       });
507     }
508   }
509
510   ngOnInit(): void {
511     this.action = this.actionLabels.CREATE;
512     this.resolveRoute();
513
514     this.cephServiceService
515       .list(new HttpParams({ fromObject: { limit: -1, offset: 0 } }))
516       .observable.subscribe((services: CephServiceSpec[]) => {
517         this.serviceList = services;
518         this.services = services.filter((service: any) =>
519           this.INGRESS_SUPPORTED_SERVICE_TYPES.includes(service.service_type)
520         );
521       });
522
523     this.cephServiceService.getKnownTypes().subscribe((resp: Array<string>) => {
524       // Remove service types:
525       // osd       - This is deployed a different way.
526       // container - This should only be used in the CLI.
527       this.hiddenServices.push('osd', 'container');
528
529       this.serviceTypes = _.difference(resp, this.hiddenServices).sort();
530     });
531     this.hostService.getAllHosts().subscribe((resp: object[]) => {
532       const options: SelectOption[] = [];
533       _.forEach(resp, (host: object) => {
534         if (_.get(host, 'sources.orchestrator', false)) {
535           const option = new SelectOption(false, _.get(host, 'hostname'), '');
536           options.push(option);
537         }
538       });
539       this.hosts.options = [...options];
540     });
541     this.hostService.getLabels().subscribe((resp: string[]) => {
542       this.labels = resp;
543     });
544     this.poolService.getList().subscribe((resp: Pool[]) => {
545       this.pools = resp;
546       this.rbdPools = this.pools.filter(this.rbdService.isRBDPool);
547       if (!this.editing && this.serviceType) {
548         this.onServiceTypeChange(this.serviceType);
549       }
550     });
551
552     if (this.editing) {
553       this.action = this.actionLabels.EDIT;
554       this.disableForEditing(this.serviceType);
555       this.cephServiceService
556         .list(new HttpParams({ fromObject: { limit: -1, offset: 0 } }), this.serviceName)
557         .observable.subscribe((response: CephServiceSpec[]) => {
558           const formKeys = ['service_type', 'service_id', 'unmanaged'];
559           formKeys.forEach((keys) => {
560             this.serviceForm.get(keys).setValue(response[0][keys]);
561           });
562           if (!response[0]['unmanaged']) {
563             const placementKey = Object.keys(response[0]['placement'])[0];
564             let placementValue: string;
565             ['hosts', 'label'].indexOf(placementKey) >= 0
566               ? (placementValue = placementKey)
567               : (placementValue = 'hosts');
568             this.serviceForm.get('placement').setValue(placementValue);
569             this.serviceForm.get('count').setValue(response[0]['placement']['count']);
570             if (response[0]?.placement[placementValue]) {
571               this.serviceForm.get(placementValue).setValue(response[0]?.placement[placementValue]);
572             }
573           }
574           switch (this.serviceType) {
575             case 'iscsi':
576               const specKeys = ['pool', 'api_password', 'api_user', 'trusted_ip_list', 'api_port'];
577               specKeys.forEach((key) => {
578                 this.serviceForm.get(key).setValue(response[0].spec[key]);
579               });
580               this.serviceForm.get('ssl').setValue(response[0].spec?.api_secure);
581               if (response[0].spec?.api_secure) {
582                 this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert);
583                 this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key);
584               }
585               break;
586             case 'nvmeof':
587               this.serviceForm.get('pool').setValue(response[0].spec.pool);
588               this.serviceForm.get('group').setValue(response[0].spec.group);
589               break;
590             case 'rgw':
591               this.serviceForm
592                 .get('rgw_frontend_port')
593                 .setValue(response[0].spec?.rgw_frontend_port);
594               this.setRgwFields(
595                 response[0].spec?.rgw_realm,
596                 response[0].spec?.rgw_zonegroup,
597                 response[0].spec?.rgw_zone
598               );
599               this.serviceForm.get('ssl').setValue(response[0].spec?.ssl);
600               if (response[0].spec?.ssl) {
601                 this.serviceForm
602                   .get('ssl_cert')
603                   .setValue(response[0].spec?.rgw_frontend_ssl_certificate);
604               }
605               break;
606             case 'ingress':
607               const ingressSpecKeys = [
608                 'backend_service',
609                 'virtual_ip',
610                 'frontend_port',
611                 'monitor_port',
612                 'virtual_interface_networks',
613                 'ssl'
614               ];
615               ingressSpecKeys.forEach((key) => {
616                 this.serviceForm.get(key).setValue(response[0].spec[key]);
617               });
618               if (response[0].spec?.ssl) {
619                 this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert);
620                 this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key);
621               }
622               break;
623             case 'smb':
624               const smbSpecKeys = [
625                 'cluster_id',
626                 'config_uri',
627                 'features',
628                 'join_sources',
629                 'user_sources',
630                 'custom_dns',
631                 'include_ceph_users'
632               ];
633               smbSpecKeys.forEach((key) => {
634                 if (key === 'features') {
635                   if (response[0].spec?.features) {
636                     response[0].spec.features.forEach((feature) => {
637                       this.serviceForm.get(`features.${feature}`).setValue(true);
638                     });
639                   }
640                 } else {
641                   this.serviceForm.get(key).setValue(response[0].spec[key]);
642                 }
643               });
644               break;
645             case 'snmp-gateway':
646               const snmpCommonSpecKeys = ['snmp_version', 'snmp_destination'];
647               snmpCommonSpecKeys.forEach((key) => {
648                 this.serviceForm.get(key).setValue(response[0].spec[key]);
649               });
650               if (this.serviceForm.getValue('snmp_version') === 'V3') {
651                 const snmpV3SpecKeys = [
652                   'engine_id',
653                   'auth_protocol',
654                   'privacy_protocol',
655                   'snmp_v3_auth_username',
656                   'snmp_v3_auth_password',
657                   'snmp_v3_priv_password'
658                 ];
659                 snmpV3SpecKeys.forEach((key) => {
660                   if (key !== null) {
661                     if (
662                       key === 'snmp_v3_auth_username' ||
663                       key === 'snmp_v3_auth_password' ||
664                       key === 'snmp_v3_priv_password'
665                     ) {
666                       this.serviceForm.get(key).setValue(response[0].spec['credentials'][key]);
667                     } else {
668                       this.serviceForm.get(key).setValue(response[0].spec[key]);
669                     }
670                   }
671                 });
672               } else {
673                 this.serviceForm
674                   .get('snmp_community')
675                   .setValue(response[0].spec['credentials']['snmp_community']);
676               }
677               break;
678             case 'grafana':
679               this.serviceForm.get('grafana_port').setValue(response[0].spec.port);
680               this.serviceForm
681                 .get('grafana_admin_password')
682                 .setValue(response[0].spec.initial_admin_password);
683               break;
684             case 'oauth2-proxy':
685               const oauth2SpecKeys = [
686                 'https_address',
687                 'provider_display_name',
688                 'client_id',
689                 'client_secret',
690                 'oidc_issuer_url',
691                 'redirect_url',
692                 'allowlist_domains'
693               ];
694               oauth2SpecKeys.forEach((key) => {
695                 this.serviceForm.get(key).setValue(response[0].spec[key]);
696               });
697               if (response[0].spec?.ssl) {
698                 this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert);
699                 this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key);
700               }
701           }
702         });
703     }
704   }
705
706   getDefaultsEntitiesForRgw(
707     defaultRealmId: string,
708     defaultZonegroupId: string,
709     defaultZoneId: string
710   ): { defaultRealmName: string; defaultZonegroupName: string; defaultZoneName: string } {
711     const defaultRealm = this.realmList.find((x: { id: string }) => x.id === defaultRealmId);
712     const defaultZonegroup = this.zonegroupList.find(
713       (x: { id: string }) => x.id === defaultZonegroupId
714     );
715     const defaultZone = this.zoneList.find((x: { id: string }) => x.id === defaultZoneId);
716     const defaultRealmName = defaultRealm !== undefined ? defaultRealm.name : null;
717     const defaultZonegroupName = defaultZonegroup !== undefined ? defaultZonegroup.name : 'default';
718     const defaultZoneName = defaultZone !== undefined ? defaultZone.name : 'default';
719     if (defaultZonegroupName === 'default' && !this.zonegroupNames.includes(defaultZonegroupName)) {
720       const defaultZonegroup = new RgwZonegroup();
721       defaultZonegroup.name = 'default';
722       this.zonegroupList.push(defaultZonegroup);
723     }
724     if (defaultZoneName === 'default' && !this.zoneNames.includes(defaultZoneName)) {
725       const defaultZone = new RgwZone();
726       defaultZone.name = 'default';
727       this.zoneList.push(defaultZone);
728     }
729     return {
730       defaultRealmName: defaultRealmName,
731       defaultZonegroupName: defaultZonegroupName,
732       defaultZoneName: defaultZoneName
733     };
734   }
735
736   getDefaultPlacementCount(serviceType: string) {
737     /**
738      * `defaults` from src/pybind/mgr/cephadm/module.py
739      */
740     switch (serviceType) {
741       case 'mon':
742         this.serviceForm.get('count').setValue(5);
743         break;
744       case 'mgr':
745       case 'mds':
746       case 'rgw':
747       case 'ingress':
748       case 'rbd-mirror':
749         this.serviceForm.get('count').setValue(2);
750         break;
751       case 'iscsi':
752       case 'nvmeof':
753       case 'cephfs-mirror':
754       case 'nfs':
755       case 'grafana':
756       case 'alertmanager':
757       case 'prometheus':
758       case 'loki':
759       case 'container':
760       case 'snmp-gateway':
761       case 'elastic-serach':
762       case 'jaeger-collector':
763       case 'jaeger-query':
764       case 'smb':
765       case 'oauth2-proxy':
766         this.serviceForm.get('count').setValue(1);
767         break;
768       default:
769         this.serviceForm.get('count').setValue(null);
770     }
771   }
772
773   setRgwFields(realm_name?: string, zonegroup_name?: string, zone_name?: string) {
774     const observables = [
775       this.rgwRealmService.getAllRealmsInfo(),
776       this.rgwZonegroupService.getAllZonegroupsInfo(),
777       this.rgwZoneService.getAllZonesInfo()
778     ];
779     this.sub = forkJoin(observables).subscribe(
780       (multisiteInfo: [object, object, object]) => {
781         this.multisiteInfo = multisiteInfo;
782         this.realmList =
783           this.multisiteInfo[0] !== undefined && this.multisiteInfo[0].hasOwnProperty('realms')
784             ? this.multisiteInfo[0]['realms']
785             : [];
786         this.zonegroupList =
787           this.multisiteInfo[1] !== undefined && this.multisiteInfo[1].hasOwnProperty('zonegroups')
788             ? this.multisiteInfo[1]['zonegroups']
789             : [];
790         this.zoneList =
791           this.multisiteInfo[2] !== undefined && this.multisiteInfo[2].hasOwnProperty('zones')
792             ? this.multisiteInfo[2]['zones']
793             : [];
794         this.realmNames = this.realmList.map((realm) => {
795           return realm['name'];
796         });
797         this.zonegroupNames = this.zonegroupList.map((zonegroup) => {
798           return zonegroup['name'];
799         });
800         this.zoneNames = this.zoneList.map((zone) => {
801           return zone['name'];
802         });
803         this.defaultRealmId = multisiteInfo[0]['default_realm'];
804         this.defaultZonegroupId = multisiteInfo[1]['default_zonegroup'];
805         this.defaultZoneId = multisiteInfo[2]['default_zone'];
806         this.defaultsInfo = this.getDefaultsEntitiesForRgw(
807           this.defaultRealmId,
808           this.defaultZonegroupId,
809           this.defaultZoneId
810         );
811         if (!this.editing) {
812           this.serviceForm.get('realm_name').setValue(this.defaultsInfo['defaultRealmName']);
813           this.serviceForm
814             .get('zonegroup_name')
815             .setValue(this.defaultsInfo['defaultZonegroupName']);
816           this.serviceForm.get('zone_name').setValue(this.defaultsInfo['defaultZoneName']);
817         } else {
818           if (realm_name && !this.realmNames.includes(realm_name)) {
819             const realm = new RgwRealm();
820             realm.name = realm_name;
821             this.realmList.push(realm);
822           }
823           if (zonegroup_name && !this.zonegroupNames.includes(zonegroup_name)) {
824             const zonegroup = new RgwZonegroup();
825             zonegroup.name = zonegroup_name;
826             this.zonegroupList.push(zonegroup);
827           }
828           if (zone_name && !this.zoneNames.includes(zone_name)) {
829             const zone = new RgwZone();
830             zone.name = zone_name;
831             this.zoneList.push(zone);
832           }
833           if (zonegroup_name === undefined && zone_name === undefined) {
834             zonegroup_name = 'default';
835             zone_name = 'default';
836           }
837           this.serviceForm.get('realm_name').setValue(realm_name);
838           this.serviceForm.get('zonegroup_name').setValue(zonegroup_name);
839           this.serviceForm.get('zone_name').setValue(zone_name);
840         }
841         if (this.realmList.length === 0) {
842           this.showRealmCreationForm = true;
843         } else {
844           this.showRealmCreationForm = false;
845         }
846       },
847       (_error) => {
848         const defaultZone = new RgwZone();
849         defaultZone.name = 'default';
850         const defaultZonegroup = new RgwZonegroup();
851         defaultZonegroup.name = 'default';
852         this.zoneList.push(defaultZone);
853         this.zonegroupList.push(defaultZonegroup);
854       }
855     );
856   }
857
858   setNvmeofServiceId(): void {
859     const defaultRbdPool: string = this.rbdPools?.find((p: Pool) => p.pool_name === 'rbd')
860       ?.pool_name;
861     if (defaultRbdPool) {
862       this.serviceForm.get('pool').setValue(defaultRbdPool);
863     }
864   }
865
866   onNvmeofGroupChange(groupName: string) {
867     this.serviceForm.get('service_id').setValue(groupName);
868   }
869
870   requiresServiceId(serviceType: string) {
871     return ['mds', 'rgw', 'nfs', 'iscsi', 'nvmeof', 'smb', 'ingress'].includes(serviceType);
872   }
873
874   setServiceId(serviceId: string): void {
875     const requiresServiceId: boolean = this.requiresServiceId(serviceId);
876     if (requiresServiceId && serviceId === 'nvmeof') {
877       this.setNvmeofServiceId();
878     } else if (requiresServiceId) {
879       this.serviceForm.get('service_id').setValue(null);
880     } else {
881       this.serviceForm.get('service_id').setValue(serviceId);
882     }
883   }
884
885   onServiceTypeChange(selectedServiceType: string) {
886     this.setServiceId(selectedServiceType);
887
888     this.serviceIds = this.serviceList
889       ?.filter((service) => service['service_type'] === selectedServiceType)
890       .map((service) => service['service_id']);
891
892     this.getDefaultPlacementCount(selectedServiceType);
893
894     if (selectedServiceType === 'rgw') {
895       this.setRgwFields();
896     }
897   }
898
899   onPlacementChange(selected: string) {
900     if (selected === 'label') {
901       this.serviceForm.get('count').setValue(null);
902     }
903   }
904
905   onBlockPoolChange() {
906     const selectedBlockPool = this.serviceForm.get('pool').value;
907     if (selectedBlockPool) {
908       this.serviceForm.get('service_id').setValue(selectedBlockPool);
909     } else {
910       this.serviceForm.get('service_id').setValue(null);
911     }
912   }
913
914   disableForEditing(serviceType: string) {
915     const disableForEditKeys = ['service_type', 'service_id'];
916     disableForEditKeys.forEach((key) => {
917       this.serviceForm.get(key).disable();
918     });
919     switch (serviceType) {
920       case 'ingress':
921         this.serviceForm.get('backend_service').disable();
922         break;
923       case 'nvmeof':
924         this.serviceForm.get('pool').disable();
925         this.serviceForm.get('group').disable();
926         break;
927     }
928   }
929
930   searchLabels = (text$: Observable<string>) => {
931     return merge(
932       text$.pipe(debounceTime(200), distinctUntilChanged()),
933       this.labelFocus,
934       this.labelClick.pipe(filter(() => !this.typeahead.isPopupOpen()))
935     ).pipe(
936       map((value) =>
937         this.labels
938           .filter((label: string) => label.toLowerCase().indexOf(value.toLowerCase()) > -1)
939           .slice(0, 10)
940       )
941     );
942   };
943
944   fileUpload(files: FileList, controlName: string) {
945     const file: File = files[0];
946     const reader = new FileReader();
947     reader.addEventListener('load', (event: ProgressEvent<FileReader>) => {
948       const control: AbstractControl = this.serviceForm.get(controlName);
949       control.setValue(event.target.result);
950       control.markAsDirty();
951       control.markAsTouched();
952       control.updateValueAndValidity();
953     });
954     reader.readAsText(file, 'utf8');
955   }
956
957   prePopulateId() {
958     const control: AbstractControl = this.serviceForm.get('service_id');
959     const backendService = this.serviceForm.getValue('backend_service');
960     // Set Id as read-only
961     control.reset({ value: backendService, disabled: true });
962   }
963
964   onSubmit() {
965     const self = this;
966     const values: object = this.serviceForm.getRawValue();
967     const serviceType: string = values['service_type'];
968     let taskUrl = `service/${URLVerbs.CREATE}`;
969     if (this.editing) {
970       taskUrl = `service/${URLVerbs.EDIT}`;
971     }
972     const serviceSpec: object = {
973       service_type: serviceType,
974       placement: {},
975       unmanaged: values['unmanaged']
976     };
977     if (serviceType === 'rgw') {
978       serviceSpec['rgw_realm'] = values['realm_name'] ? values['realm_name'] : null;
979       serviceSpec['rgw_zonegroup'] =
980         values['zonegroup_name'] !== 'default' ? values['zonegroup_name'] : null;
981       serviceSpec['rgw_zone'] = values['zone_name'] !== 'default' ? values['zone_name'] : null;
982     }
983
984     const serviceId: string = values['service_id'];
985     let serviceName: string = serviceType;
986     if (_.isString(serviceId) && !_.isEmpty(serviceId) && serviceId !== serviceType) {
987       serviceName = `${serviceType}.${serviceId}`;
988       serviceSpec['service_id'] = serviceId;
989     }
990
991     // These services has some fields to be
992     // filled out even if unmanaged is true
993     switch (serviceType) {
994       case 'ingress':
995         serviceSpec['backend_service'] = values['backend_service'];
996         serviceSpec['service_id'] = values['backend_service'];
997         if (_.isNumber(values['frontend_port']) && values['frontend_port'] > 0) {
998           serviceSpec['frontend_port'] = values['frontend_port'];
999         }
1000         if (_.isString(values['virtual_ip']) && !_.isEmpty(values['virtual_ip'])) {
1001           serviceSpec['virtual_ip'] = values['virtual_ip'].trim();
1002         }
1003         if (_.isNumber(values['monitor_port']) && values['monitor_port'] > 0) {
1004           serviceSpec['monitor_port'] = values['monitor_port'];
1005         }
1006         break;
1007
1008       case 'nvmeof':
1009         serviceSpec['pool'] = values['pool'];
1010         serviceSpec['group'] = values['group'];
1011         break;
1012       case 'iscsi':
1013         serviceSpec['pool'] = values['pool'];
1014         break;
1015
1016       case 'smb':
1017         serviceSpec['cluster_id'] = values['cluster_id']?.trim();
1018         serviceSpec['config_uri'] = values['config_uri']?.trim();
1019         for (const feature in values['features']) {
1020           if (values['features'][feature]) {
1021             (serviceSpec['features'] = serviceSpec['features'] || []).push(feature);
1022           }
1023         }
1024         serviceSpec['custom_dns'] = values['custom_dns']?.trim();
1025         serviceSpec['join_sources'] = values['join_sources']?.trim();
1026         serviceSpec['user_sources'] = values['user_sources']?.trim();
1027         serviceSpec['include_ceph_users'] = values['include_ceph_users']?.trim();
1028         break;
1029
1030       case 'snmp-gateway':
1031         serviceSpec['credentials'] = {};
1032         serviceSpec['snmp_version'] = values['snmp_version'];
1033         serviceSpec['snmp_destination'] = values['snmp_destination'];
1034         if (values['snmp_version'] === 'V3') {
1035           serviceSpec['engine_id'] = values['engine_id'];
1036           serviceSpec['auth_protocol'] = values['auth_protocol'];
1037           serviceSpec['credentials']['snmp_v3_auth_username'] = values['snmp_v3_auth_username'];
1038           serviceSpec['credentials']['snmp_v3_auth_password'] = values['snmp_v3_auth_password'];
1039           if (values['privacy_protocol'] !== null) {
1040             serviceSpec['privacy_protocol'] = values['privacy_protocol'];
1041             serviceSpec['credentials']['snmp_v3_priv_password'] = values['snmp_v3_priv_password'];
1042           }
1043         } else {
1044           serviceSpec['credentials']['snmp_community'] = values['snmp_community'];
1045         }
1046         break;
1047     }
1048
1049     if (!values['unmanaged']) {
1050       switch (values['placement']) {
1051         case 'hosts':
1052           if (values['hosts'].length > 0) {
1053             serviceSpec['placement']['hosts'] = values['hosts'];
1054           }
1055           break;
1056         case 'label':
1057           serviceSpec['placement']['label'] = values['label'];
1058           break;
1059       }
1060       if (_.isNumber(values['count']) && values['count'] > 0) {
1061         serviceSpec['placement']['count'] = values['count'];
1062       }
1063       switch (serviceType) {
1064         case 'rgw':
1065           if (_.isNumber(values['rgw_frontend_port']) && values['rgw_frontend_port'] > 0) {
1066             serviceSpec['rgw_frontend_port'] = values['rgw_frontend_port'];
1067           }
1068           serviceSpec['ssl'] = values['ssl'];
1069           if (values['ssl']) {
1070             serviceSpec['rgw_frontend_ssl_certificate'] = values['ssl_cert']?.trim();
1071           }
1072           break;
1073         case 'iscsi':
1074           if (_.isString(values['trusted_ip_list']) && !_.isEmpty(values['trusted_ip_list'])) {
1075             serviceSpec['trusted_ip_list'] = values['trusted_ip_list'].trim();
1076           }
1077           if (_.isNumber(values['api_port']) && values['api_port'] > 0) {
1078             serviceSpec['api_port'] = values['api_port'];
1079           }
1080           serviceSpec['api_user'] = values['api_user'];
1081           serviceSpec['api_password'] = values['api_password'];
1082           serviceSpec['api_secure'] = values['ssl'];
1083           if (values['ssl']) {
1084             serviceSpec['ssl_cert'] = values['ssl_cert']?.trim();
1085             serviceSpec['ssl_key'] = values['ssl_key']?.trim();
1086           }
1087           break;
1088         case 'ingress':
1089           serviceSpec['ssl'] = values['ssl'];
1090           if (values['ssl']) {
1091             serviceSpec['ssl_cert'] = values['ssl_cert']?.trim();
1092             serviceSpec['ssl_key'] = values['ssl_key']?.trim();
1093           }
1094           serviceSpec['virtual_interface_networks'] = values['virtual_interface_networks'];
1095           break;
1096         case 'grafana':
1097           serviceSpec['port'] = values['grafana_port'];
1098           serviceSpec['initial_admin_password'] = values['grafana_admin_password'];
1099           break;
1100         case 'oauth2-proxy':
1101           serviceSpec['provider_display_name'] = values['provider_display_name']?.trim();
1102           serviceSpec['client_id'] = values['client_id']?.trim();
1103           serviceSpec['client_secret'] = values['client_secret']?.trim();
1104           serviceSpec['oidc_issuer_url'] = values['oidc_issuer_url']?.trim();
1105           serviceSpec['https_address'] = values['https_address']?.trim();
1106           serviceSpec['redirect_url'] = values['redirect_url']?.trim();
1107           serviceSpec['allowlist_domains'] = values['allowlist_domains']?.trim().split(',');
1108           if (values['ssl']) {
1109             serviceSpec['ssl_cert'] = values['ssl_cert']?.trim();
1110             serviceSpec['ssl_key'] = values['ssl_key']?.trim();
1111           }
1112           break;
1113       }
1114     }
1115     this.taskWrapperService
1116       .wrapTaskAroundCall({
1117         task: new FinishedTask(taskUrl, {
1118           service_name: serviceName
1119         }),
1120         call: this.editing
1121           ? this.cephServiceService.update(serviceSpec)
1122           : this.cephServiceService.create(serviceSpec)
1123       })
1124       .subscribe({
1125         error() {
1126           self.serviceForm.setErrors({ cdSubmitButton: true });
1127         },
1128         complete: () => {
1129           this.pageURL === 'services'
1130             ? this.router.navigate([this.pageURL, { outlets: { modal: null } }])
1131             : this.activeModal.close();
1132         }
1133       });
1134   }
1135
1136   clearValidations() {
1137     const snmpVersion = this.serviceForm.getValue('snmp_version');
1138     const privacyProtocol = this.serviceForm.getValue('privacy_protocol');
1139     if (snmpVersion === 'V3') {
1140       this.serviceForm.get('snmp_community').clearValidators();
1141     } else {
1142       this.serviceForm.get('engine_id').clearValidators();
1143       this.serviceForm.get('auth_protocol').clearValidators();
1144       this.serviceForm.get('privacy_protocol').clearValidators();
1145       this.serviceForm.get('snmp_v3_auth_username').clearValidators();
1146       this.serviceForm.get('snmp_v3_auth_password').clearValidators();
1147     }
1148     if (privacyProtocol === null) {
1149       this.serviceForm.get('snmp_v3_priv_password').clearValidators();
1150     }
1151   }
1152
1153   createMultisiteSetup() {
1154     this.bsModalRef = this.modalService.show(CreateRgwServiceEntitiesComponent, {
1155       size: 'lg'
1156     });
1157     this.bsModalRef.componentInstance.submitAction.subscribe(() => {
1158       this.setRgwFields();
1159     });
1160   }
1161 }