]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/blob
e25bf9a34ee77b026ea7e0841db3b5366152749d
[ceph-ci.git] /
1 import { Component, Input, OnInit, ViewChild } from '@angular/core';
2 import { AbstractControl, Validators } from '@angular/forms';
3 import { ActivatedRoute, Router } from '@angular/router';
4
5 import { NgbActiveModal, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
6 import _ from 'lodash';
7 import { merge, Observable, Subject } from 'rxjs';
8 import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators';
9
10 import { CephServiceService } from '~/app/shared/api/ceph-service.service';
11 import { HostService } from '~/app/shared/api/host.service';
12 import { PoolService } from '~/app/shared/api/pool.service';
13 import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
14 import { SelectOption } from '~/app/shared/components/select/select-option.model';
15 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
16 import { CdForm } from '~/app/shared/forms/cd-form';
17 import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
18 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
19 import { CdValidators } from '~/app/shared/forms/cd-validators';
20 import { FinishedTask } from '~/app/shared/models/finished-task';
21 import { CephServiceSpec } from '~/app/shared/models/service.interface';
22 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
23
24 @Component({
25   selector: 'cd-service-form',
26   templateUrl: './service-form.component.html',
27   styleUrls: ['./service-form.component.scss']
28 })
29 export class ServiceFormComponent extends CdForm implements OnInit {
30   readonly RGW_SVC_ID_PATTERN = /^([^.]+)(\.([^.]+)\.([^.]+))?$/;
31   readonly MDS_SVC_ID_PATTERN = /^[a-zA-Z_.-][a-zA-Z0-9_.-]*$/;
32   readonly SNMP_DESTINATION_PATTERN = /^[^\:]+:[0-9]/;
33   readonly SNMP_ENGINE_ID_PATTERN = /^[0-9A-Fa-f]{10,64}/g;
34   readonly INGRESS_SUPPORTED_SERVICE_TYPES = ['rgw', 'nfs'];
35   @ViewChild(NgbTypeahead, { static: false })
36   typeahead: NgbTypeahead;
37
38   @Input() hiddenServices: string[] = [];
39
40   @Input() editing = false;
41
42   @Input() serviceName: string;
43
44   @Input() serviceType: string;
45
46   serviceForm: CdFormGroup;
47   action: string;
48   resource: string;
49   serviceTypes: string[] = [];
50   serviceIds: string[] = [];
51   hosts: any;
52   labels: string[];
53   labelClick = new Subject<string>();
54   labelFocus = new Subject<string>();
55   pools: Array<object>;
56   services: Array<CephServiceSpec> = [];
57   pageURL: string;
58   serviceList: CephServiceSpec[];
59
60   constructor(
61     public actionLabels: ActionLabelsI18n,
62     private cephServiceService: CephServiceService,
63     private formBuilder: CdFormBuilder,
64     private hostService: HostService,
65     private poolService: PoolService,
66     private router: Router,
67     private taskWrapperService: TaskWrapperService,
68     private route: ActivatedRoute,
69     public activeModal: NgbActiveModal
70   ) {
71     super();
72     this.resource = $localize`service`;
73     this.hosts = {
74       options: [],
75       messages: new SelectMessages({
76         empty: $localize`There are no hosts.`,
77         filter: $localize`Filter hosts`
78       })
79     };
80     this.createForm();
81   }
82
83   createForm() {
84     this.serviceForm = this.formBuilder.group({
85       // Global
86       service_type: [null, [Validators.required]],
87       service_id: [
88         null,
89         [
90           CdValidators.composeIf(
91             {
92               service_type: 'mds'
93             },
94             [
95               Validators.required,
96               CdValidators.custom('mdsPattern', (value: string) => {
97                 if (_.isEmpty(value)) {
98                   return false;
99                 }
100                 return !this.MDS_SVC_ID_PATTERN.test(value);
101               })
102             ]
103           ),
104           CdValidators.requiredIf({
105             service_type: 'nfs'
106           }),
107           CdValidators.requiredIf({
108             service_type: 'iscsi'
109           }),
110           CdValidators.requiredIf({
111             service_type: 'ingress'
112           }),
113           CdValidators.composeIf(
114             {
115               service_type: 'rgw'
116             },
117             [
118               Validators.required,
119               CdValidators.custom('rgwPattern', (value: string) => {
120                 if (_.isEmpty(value)) {
121                   return false;
122                 }
123                 return !this.RGW_SVC_ID_PATTERN.test(value);
124               })
125             ]
126           ),
127           CdValidators.custom('uniqueName', (service_id: string) => {
128             return this.serviceIds && this.serviceIds.includes(service_id);
129           })
130         ]
131       ],
132       placement: ['hosts'],
133       label: [
134         null,
135         [
136           CdValidators.requiredIf({
137             placement: 'label',
138             unmanaged: false
139           })
140         ]
141       ],
142       hosts: [[]],
143       count: [null, [CdValidators.number(false)]],
144       unmanaged: [false],
145       // iSCSI
146       pool: [
147         null,
148         [
149           CdValidators.requiredIf({
150             service_type: 'iscsi',
151             unmanaged: false
152           })
153         ]
154       ],
155       // RGW
156       rgw_frontend_port: [null, [CdValidators.number(false)]],
157       // iSCSI
158       trusted_ip_list: [null],
159       api_port: [null, [CdValidators.number(false)]],
160       api_user: [
161         null,
162         [
163           CdValidators.requiredIf({
164             service_type: 'iscsi',
165             unmanaged: false
166           })
167         ]
168       ],
169       api_password: [
170         null,
171         [
172           CdValidators.requiredIf({
173             service_type: 'iscsi',
174             unmanaged: false
175           })
176         ]
177       ],
178       // Ingress
179       backend_service: [
180         null,
181         [
182           CdValidators.requiredIf({
183             service_type: 'ingress'
184           })
185         ]
186       ],
187       virtual_ip: [
188         null,
189         [
190           CdValidators.requiredIf({
191             service_type: 'ingress'
192           })
193         ]
194       ],
195       frontend_port: [
196         null,
197         [
198           CdValidators.number(false),
199           CdValidators.requiredIf({
200             service_type: 'ingress'
201           })
202         ]
203       ],
204       monitor_port: [
205         null,
206         [
207           CdValidators.number(false),
208           CdValidators.requiredIf({
209             service_type: 'ingress'
210           })
211         ]
212       ],
213       virtual_interface_networks: [null],
214       // RGW, Ingress & iSCSI
215       ssl: [false],
216       ssl_cert: [
217         '',
218         [
219           CdValidators.composeIf(
220             {
221               service_type: 'rgw',
222               unmanaged: false,
223               ssl: true
224             },
225             [Validators.required, CdValidators.pemCert()]
226           ),
227           CdValidators.composeIf(
228             {
229               service_type: 'iscsi',
230               unmanaged: false,
231               ssl: true
232             },
233             [Validators.required, CdValidators.sslCert()]
234           ),
235           CdValidators.composeIf(
236             {
237               service_type: 'ingress',
238               unmanaged: false,
239               ssl: true
240             },
241             [Validators.required, CdValidators.pemCert()]
242           )
243         ]
244       ],
245       ssl_key: [
246         '',
247         [
248           CdValidators.composeIf(
249             {
250               service_type: 'iscsi',
251               unmanaged: false,
252               ssl: true
253             },
254             [Validators.required, CdValidators.sslPrivKey()]
255           )
256         ]
257       ],
258       // snmp-gateway
259       snmp_version: [
260         null,
261         [
262           CdValidators.requiredIf({
263             service_type: 'snmp-gateway'
264           })
265         ]
266       ],
267       snmp_destination: [
268         null,
269         {
270           validators: [
271             CdValidators.requiredIf({
272               service_type: 'snmp-gateway'
273             }),
274             CdValidators.custom('snmpDestinationPattern', (value: string) => {
275               if (_.isEmpty(value)) {
276                 return false;
277               }
278               return !this.SNMP_DESTINATION_PATTERN.test(value);
279             })
280           ]
281         }
282       ],
283       engine_id: [
284         null,
285         [
286           CdValidators.requiredIf({
287             service_type: 'snmp-gateway'
288           }),
289           CdValidators.custom('snmpEngineIdPattern', (value: string) => {
290             if (_.isEmpty(value)) {
291               return false;
292             }
293             return !this.SNMP_ENGINE_ID_PATTERN.test(value);
294           })
295         ]
296       ],
297       auth_protocol: [
298         'SHA',
299         [
300           CdValidators.requiredIf({
301             service_type: 'snmp-gateway'
302           })
303         ]
304       ],
305       privacy_protocol: [null],
306       snmp_community: [
307         null,
308         [
309           CdValidators.requiredIf({
310             snmp_version: 'V2c'
311           })
312         ]
313       ],
314       snmp_v3_auth_username: [
315         null,
316         [
317           CdValidators.requiredIf({
318             service_type: 'snmp-gateway'
319           })
320         ]
321       ],
322       snmp_v3_auth_password: [
323         null,
324         [
325           CdValidators.requiredIf({
326             service_type: 'snmp-gateway'
327           })
328         ]
329       ],
330       snmp_v3_priv_password: [
331         null,
332         [
333           CdValidators.requiredIf({
334             privacy_protocol: { op: '!empty' }
335           })
336         ]
337       ]
338     });
339   }
340
341   ngOnInit(): void {
342     this.action = this.actionLabels.CREATE;
343     if (this.router.url.includes('services/(modal:create')) {
344       this.pageURL = 'services';
345     } else if (this.router.url.includes('services/(modal:edit')) {
346       this.editing = true;
347       this.pageURL = 'services';
348       this.route.params.subscribe((params: { type: string; name: string }) => {
349         this.serviceName = params.name;
350         this.serviceType = params.type;
351       });
352     }
353
354     this.cephServiceService.list().subscribe((services: CephServiceSpec[]) => {
355       this.serviceList = services;
356       this.services = services.filter((service: any) =>
357         this.INGRESS_SUPPORTED_SERVICE_TYPES.includes(service.service_type)
358       );
359     });
360
361     this.cephServiceService.getKnownTypes().subscribe((resp: Array<string>) => {
362       // Remove service types:
363       // osd       - This is deployed a different way.
364       // container - This should only be used in the CLI.
365       this.hiddenServices.push('osd', 'container');
366
367       this.serviceTypes = _.difference(resp, this.hiddenServices).sort();
368     });
369     this.hostService.list('false').subscribe((resp: object[]) => {
370       const options: SelectOption[] = [];
371       _.forEach(resp, (host: object) => {
372         if (_.get(host, 'sources.orchestrator', false)) {
373           const option = new SelectOption(false, _.get(host, 'hostname'), '');
374           options.push(option);
375         }
376       });
377       this.hosts.options = [...options];
378     });
379     this.hostService.getLabels().subscribe((resp: string[]) => {
380       this.labels = resp;
381     });
382     this.poolService.getList().subscribe((resp: Array<object>) => {
383       this.pools = resp;
384     });
385
386     if (this.editing) {
387       this.action = this.actionLabels.EDIT;
388       this.disableForEditing(this.serviceType);
389       this.cephServiceService.list(this.serviceName).subscribe((response: CephServiceSpec[]) => {
390         const formKeys = ['service_type', 'service_id', 'unmanaged'];
391         formKeys.forEach((keys) => {
392           this.serviceForm.get(keys).setValue(response[0][keys]);
393         });
394         if (!response[0]['unmanaged']) {
395           const placementKey = Object.keys(response[0]['placement'])[0];
396           let placementValue: string;
397           ['hosts', 'label'].indexOf(placementKey) >= 0
398             ? (placementValue = placementKey)
399             : (placementValue = 'hosts');
400           this.serviceForm.get('placement').setValue(placementValue);
401           this.serviceForm.get('count').setValue(response[0]['placement']['count']);
402           if (response[0]?.placement[placementValue]) {
403             this.serviceForm.get(placementValue).setValue(response[0]?.placement[placementValue]);
404           }
405         }
406         switch (this.serviceType) {
407           case 'iscsi':
408             const specKeys = ['pool', 'api_password', 'api_user', 'trusted_ip_list', 'api_port'];
409             specKeys.forEach((key) => {
410               this.serviceForm.get(key).setValue(response[0].spec[key]);
411             });
412             this.serviceForm.get('ssl').setValue(response[0].spec?.api_secure);
413             if (response[0].spec?.api_secure) {
414               this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert);
415               this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key);
416             }
417             break;
418           case 'rgw':
419             this.serviceForm.get('rgw_frontend_port').setValue(response[0].spec?.rgw_frontend_port);
420             this.serviceForm.get('ssl').setValue(response[0].spec?.ssl);
421             if (response[0].spec?.ssl) {
422               this.serviceForm
423                 .get('ssl_cert')
424                 .setValue(response[0].spec?.rgw_frontend_ssl_certificate);
425             }
426             break;
427           case 'ingress':
428             const ingressSpecKeys = [
429               'backend_service',
430               'virtual_ip',
431               'frontend_port',
432               'monitor_port',
433               'virtual_interface_networks',
434               'ssl'
435             ];
436             ingressSpecKeys.forEach((key) => {
437               this.serviceForm.get(key).setValue(response[0].spec[key]);
438             });
439             if (response[0].spec?.ssl) {
440               this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert);
441               this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key);
442             }
443             break;
444           case 'snmp-gateway':
445             const snmpCommonSpecKeys = ['snmp_version', 'snmp_destination'];
446             snmpCommonSpecKeys.forEach((key) => {
447               this.serviceForm.get(key).setValue(response[0].spec[key]);
448             });
449             if (this.serviceForm.getValue('snmp_version') === 'V3') {
450               const snmpV3SpecKeys = [
451                 'engine_id',
452                 'auth_protocol',
453                 'privacy_protocol',
454                 'snmp_v3_auth_username',
455                 'snmp_v3_auth_password',
456                 'snmp_v3_priv_password'
457               ];
458               snmpV3SpecKeys.forEach((key) => {
459                 if (key !== null) {
460                   if (
461                     key === 'snmp_v3_auth_username' ||
462                     key === 'snmp_v3_auth_password' ||
463                     key === 'snmp_v3_priv_password'
464                   ) {
465                     this.serviceForm.get(key).setValue(response[0].spec['credentials'][key]);
466                   } else {
467                     this.serviceForm.get(key).setValue(response[0].spec[key]);
468                   }
469                 }
470               });
471             } else {
472               this.serviceForm
473                 .get('snmp_community')
474                 .setValue(response[0].spec['credentials']['snmp_community']);
475             }
476             break;
477         }
478       });
479     }
480   }
481
482   getServiceIds(selectedServiceType: string) {
483     this.serviceIds = this.serviceList
484       .filter((service) => service['service_type'] === selectedServiceType)
485       .map((service) => service['service_id']);
486   }
487
488   disableForEditing(serviceType: string) {
489     const disableForEditKeys = ['service_type', 'service_id'];
490     disableForEditKeys.forEach((key) => {
491       this.serviceForm.get(key).disable();
492     });
493     switch (serviceType) {
494       case 'ingress':
495         this.serviceForm.get('backend_service').disable();
496     }
497   }
498
499   searchLabels = (text$: Observable<string>) => {
500     return merge(
501       text$.pipe(debounceTime(200), distinctUntilChanged()),
502       this.labelFocus,
503       this.labelClick.pipe(filter(() => !this.typeahead.isPopupOpen()))
504     ).pipe(
505       map((value) =>
506         this.labels
507           .filter((label: string) => label.toLowerCase().indexOf(value.toLowerCase()) > -1)
508           .slice(0, 10)
509       )
510     );
511   };
512
513   fileUpload(files: FileList, controlName: string) {
514     const file: File = files[0];
515     const reader = new FileReader();
516     reader.addEventListener('load', (event: ProgressEvent<FileReader>) => {
517       const control: AbstractControl = this.serviceForm.get(controlName);
518       control.setValue(event.target.result);
519       control.markAsDirty();
520       control.markAsTouched();
521       control.updateValueAndValidity();
522     });
523     reader.readAsText(file, 'utf8');
524   }
525
526   prePopulateId() {
527     const control: AbstractControl = this.serviceForm.get('service_id');
528     const backendService = this.serviceForm.getValue('backend_service');
529     // Set Id as read-only
530     control.reset({ value: backendService, disabled: true });
531   }
532
533   onSubmit() {
534     const self = this;
535     const values: object = this.serviceForm.getRawValue();
536     const serviceType: string = values['service_type'];
537     let taskUrl = `service/${URLVerbs.CREATE}`;
538     if (this.editing) {
539       taskUrl = `service/${URLVerbs.EDIT}`;
540     }
541     const serviceSpec: object = {
542       service_type: serviceType,
543       placement: {},
544       unmanaged: values['unmanaged']
545     };
546     let svcId: string;
547     if (serviceType === 'rgw') {
548       const svcIdMatch = values['service_id'].match(this.RGW_SVC_ID_PATTERN);
549       svcId = svcIdMatch[1];
550       if (svcIdMatch[3]) {
551         serviceSpec['rgw_realm'] = svcIdMatch[3];
552         serviceSpec['rgw_zone'] = svcIdMatch[4];
553       }
554     } else {
555       svcId = values['service_id'];
556     }
557     const serviceId: string = svcId;
558     let serviceName: string = serviceType;
559     if (_.isString(serviceId) && !_.isEmpty(serviceId)) {
560       serviceName = `${serviceType}.${serviceId}`;
561       serviceSpec['service_id'] = serviceId;
562     }
563
564     switch (serviceType) {
565       case 'ingress':
566         serviceSpec['backend_service'] = values['backend_service'];
567         serviceSpec['service_id'] = values['backend_service'];
568         if (_.isNumber(values['frontend_port']) && values['frontend_port'] > 0) {
569           serviceSpec['frontend_port'] = values['frontend_port'];
570         }
571         if (_.isString(values['virtual_ip']) && !_.isEmpty(values['virtual_ip'])) {
572           serviceSpec['virtual_ip'] = values['virtual_ip'].trim();
573         }
574         if (_.isNumber(values['monitor_port']) && values['monitor_port'] > 0) {
575           serviceSpec['monitor_port'] = values['monitor_port'];
576         }
577         break;
578     }
579
580     if (!values['unmanaged']) {
581       switch (values['placement']) {
582         case 'hosts':
583           if (values['hosts'].length > 0) {
584             serviceSpec['placement']['hosts'] = values['hosts'];
585           }
586           break;
587         case 'label':
588           serviceSpec['placement']['label'] = values['label'];
589           break;
590       }
591       if (_.isNumber(values['count']) && values['count'] > 0) {
592         serviceSpec['placement']['count'] = values['count'];
593       }
594       switch (serviceType) {
595         case 'rgw':
596           if (_.isNumber(values['rgw_frontend_port']) && values['rgw_frontend_port'] > 0) {
597             serviceSpec['rgw_frontend_port'] = values['rgw_frontend_port'];
598           }
599           serviceSpec['ssl'] = values['ssl'];
600           if (values['ssl']) {
601             serviceSpec['rgw_frontend_ssl_certificate'] = values['ssl_cert']?.trim();
602           }
603           break;
604         case 'iscsi':
605           serviceSpec['pool'] = values['pool'];
606           if (_.isString(values['trusted_ip_list']) && !_.isEmpty(values['trusted_ip_list'])) {
607             serviceSpec['trusted_ip_list'] = values['trusted_ip_list'].trim();
608           }
609           if (_.isNumber(values['api_port']) && values['api_port'] > 0) {
610             serviceSpec['api_port'] = values['api_port'];
611           }
612           serviceSpec['api_user'] = values['api_user'];
613           serviceSpec['api_password'] = values['api_password'];
614           serviceSpec['api_secure'] = values['ssl'];
615           if (values['ssl']) {
616             serviceSpec['ssl_cert'] = values['ssl_cert']?.trim();
617             serviceSpec['ssl_key'] = values['ssl_key']?.trim();
618           }
619           break;
620         case 'ingress':
621           serviceSpec['ssl'] = values['ssl'];
622           if (values['ssl']) {
623             serviceSpec['ssl_cert'] = values['ssl_cert']?.trim();
624             serviceSpec['ssl_key'] = values['ssl_key']?.trim();
625           }
626           serviceSpec['virtual_interface_networks'] = values['virtual_interface_networks'];
627           break;
628         case 'snmp-gateway':
629           serviceSpec['credentials'] = {};
630           serviceSpec['snmp_version'] = values['snmp_version'];
631           serviceSpec['snmp_destination'] = values['snmp_destination'];
632           if (values['snmp_version'] === 'V3') {
633             serviceSpec['engine_id'] = values['engine_id'];
634             serviceSpec['auth_protocol'] = values['auth_protocol'];
635             serviceSpec['credentials']['snmp_v3_auth_username'] = values['snmp_v3_auth_username'];
636             serviceSpec['credentials']['snmp_v3_auth_password'] = values['snmp_v3_auth_password'];
637             if (values['privacy_protocol'] !== null) {
638               serviceSpec['privacy_protocol'] = values['privacy_protocol'];
639               serviceSpec['credentials']['snmp_v3_priv_password'] = values['snmp_v3_priv_password'];
640             }
641           } else {
642             serviceSpec['credentials']['snmp_community'] = values['snmp_community'];
643           }
644           break;
645       }
646     }
647
648     this.taskWrapperService
649       .wrapTaskAroundCall({
650         task: new FinishedTask(taskUrl, {
651           service_name: serviceName
652         }),
653         call: this.editing
654           ? this.cephServiceService.update(serviceSpec)
655           : this.cephServiceService.create(serviceSpec)
656       })
657       .subscribe({
658         error() {
659           self.serviceForm.setErrors({ cdSubmitButton: true });
660         },
661         complete: () => {
662           this.pageURL === 'services'
663             ? this.router.navigate([this.pageURL, { outlets: { modal: null } }])
664             : this.activeModal.close();
665         }
666       });
667   }
668
669   clearValidations() {
670     const snmpVersion = this.serviceForm.getValue('snmp_version');
671     const privacyProtocol = this.serviceForm.getValue('privacy_protocol');
672     if (snmpVersion === 'V3') {
673       this.serviceForm.get('snmp_community').clearValidators();
674     } else {
675       this.serviceForm.get('engine_id').clearValidators();
676       this.serviceForm.get('auth_protocol').clearValidators();
677       this.serviceForm.get('privacy_protocol').clearValidators();
678       this.serviceForm.get('snmp_v3_auth_username').clearValidators();
679       this.serviceForm.get('snmp_v3_auth_password').clearValidators();
680     }
681     if (privacyProtocol === null) {
682       this.serviceForm.get('snmp_v3_priv_password').clearValidators();
683     }
684   }
685 }