1 import { Component, OnInit } from '@angular/core';
2 import { FormArray, FormControl, Validators } from '@angular/forms';
3 import { ActivatedRoute, Router } from '@angular/router';
5 import { I18n } from '@ngx-translate/i18n-polyfill';
6 import * as _ from 'lodash';
7 import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
8 import { forkJoin } from 'rxjs';
10 import { IscsiService } from '../../../shared/api/iscsi.service';
11 import { RbdService } from '../../../shared/api/rbd.service';
12 import { SelectMessages } from '../../../shared/components/select/select-messages.model';
13 import { SelectOption } from '../../../shared/components/select/select-option.model';
14 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
15 import { CdValidators } from '../../../shared/forms/cd-validators';
16 import { FinishedTask } from '../../../shared/models/finished-task';
17 import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
18 import { IscsiTargetImageSettingsModalComponent } from '../iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component';
19 import { IscsiTargetIqnSettingsModalComponent } from '../iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component';
22 selector: 'cd-iscsi-target-form',
23 templateUrl: './iscsi-target-form.component.html',
24 styleUrls: ['./iscsi-target-form.component.scss']
26 export class IscsiTargetFormComponent implements OnInit {
27 targetForm: CdFormGroup;
30 target_default_controls: any;
31 disk_default_controls: any;
37 imagesSelections: SelectOption[];
38 portalsSelections: SelectOption[] = [];
40 imagesInitiatorSelections: SelectOption[][] = [];
41 groupDiskSelections: SelectOption[][] = [];
42 groupMembersSelections: SelectOption[][] = [];
44 imagesSettings: any = {};
46 portals: new SelectMessages(
47 { noOptions: this.i18n('There are no portals available.') },
50 images: new SelectMessages(
51 { noOptions: this.i18n('There are no images available.') },
54 initiatorImage: new SelectMessages(
57 'There are no images available. Please make sure you add an image to the target.'
62 groupInitiator: new SelectMessages(
65 'There are no initiators available. Please make sure you add an initiator to the target.'
72 IQN_REGEX = /^iqn\.(19|20)\d\d-(0[1-9]|1[0-2])\.\D{2,3}(\.[A-Za-z0-9-]+)+(:[A-Za-z0-9-\.]+)*$/;
73 USER_REGEX = /[\w\.:@_-]{8,64}/;
74 PASSWORD_REGEX = /[\w@\-_]{12,16}/;
77 private iscsiService: IscsiService,
78 private modalService: BsModalService,
79 private rbdService: RbdService,
80 private router: Router,
81 private route: ActivatedRoute,
83 private taskWrapper: TaskWrapperService
87 const promises: any[] = [
88 this.iscsiService.listTargets(),
89 this.rbdService.list(),
90 this.iscsiService.portals(),
91 this.iscsiService.settings()
94 if (this.router.url.startsWith('/block/iscsi/targets/edit')) {
96 this.route.params.subscribe((params: { target_iqn: string }) => {
97 this.target_iqn = decodeURIComponent(params.target_iqn);
98 promises.push(this.iscsiService.getTarget(this.target_iqn));
102 forkJoin(promises).subscribe((data: any[]) => {
103 // iscsiService.listTargets
104 const usedImages = _(data[0])
105 .filter((target) => target.target_iqn !== this.target_iqn)
106 .flatMap((target) => target.disks)
107 .map((image) => `${image.pool}/${image.image}`)
111 this.imagesAll = _(data[1])
112 .flatMap((pool) => pool.value)
113 .map((image) => `${image.pool_name}/${image.name}`)
114 .filter((image) => usedImages.indexOf(image) === -1)
117 this.imagesSelections = this.imagesAll.map((image) => new SelectOption(false, image, ''));
119 // iscsiService.portals()
120 const portals: SelectOption[] = [];
121 data[2].forEach((portal) => {
122 portal.ip_addresses.forEach((ip) => {
123 portals.push(new SelectOption(false, portal.name + ':' + ip, ''));
126 this.portalsSelections = [...portals];
128 // iscsiService.settings()
129 this.minimum_gateways = data[3].config.minimum_gateways;
130 this.target_default_controls = data[3].target_default_controls;
131 this.disk_default_controls = data[3].disk_default_controls;
135 // iscsiService.getTarget()
137 this.resolveModel(data[4]);
143 this.targetForm = new CdFormGroup({
144 target_iqn: new FormControl('iqn.2001-07.com.ceph:' + Date.now(), {
145 validators: [Validators.required, Validators.pattern(this.IQN_REGEX)]
147 target_controls: new FormControl({}),
148 portals: new FormControl([], {
150 CdValidators.custom('minGateways', (value) => {
151 const gateways = _.uniq(value.map((elem) => elem.split(':')[0]));
152 return gateways.length < Math.max(1, this.minimum_gateways);
156 disks: new FormControl([]),
157 initiators: new FormArray([]),
158 groups: new FormArray([])
163 this.targetForm.patchValue({
164 target_iqn: res.target_iqn,
165 target_controls: res.target_controls
169 _.forEach(res.portals, (portal) => {
170 const id = `${portal.host}:${portal.ip}`;
173 this.targetForm.patchValue({
178 _.forEach(res.disks, (disk) => {
179 const id = `${disk.pool}/${disk.image}`;
181 this.imagesSettings[id] = disk.controls;
182 this.onImageSelection({ option: { name: id, selected: true } });
184 this.targetForm.patchValue({
188 _.forEach(res.clients, (client) => {
189 const initiator = this.addInitiator();
190 client.luns = _.map(client.luns, (lun) => `${lun.pool}/${lun.image}`);
191 initiator.patchValue(client);
192 // updatedInitiatorSelector()
195 _.forEach(res.groups, (group) => {
196 const fg = this.addGroup();
198 group.disks = _.map(group.disks, (disk) => `${disk.pool}/${disk.image}`);
199 fg.patchValue(group);
200 _.forEach(group.members, (member) => {
201 this.onGroupMemberSelection({ option: new SelectOption(true, member, '') });
206 hasAdvancedSettings(settings: any) {
207 return Object.values(settings).length > 0;
212 return this.targetForm.get('portals') as FormControl;
215 onPortalSelection() {
216 this.portals.setValue(this.portals.value);
219 removePortal(index: number, portal: string) {
220 this.portalsSelections.forEach((value) => {
221 if (value.name === portal) {
222 value.selected = false;
226 this.portals.value.splice(index, 1);
227 this.portals.setValue(this.portals.value);
233 return this.targetForm.get('disks') as FormControl;
236 removeImage(index: number, image: string) {
237 this.imagesSelections.forEach((value) => {
238 if (value.name === image) {
239 value.selected = false;
242 this.disks.value.splice(index, 1);
243 this.removeImageRefs(image);
247 removeImageRefs(name) {
248 this.initiators.controls.forEach((element) => {
249 const newImages = element.value.luns.filter((item) => item !== name);
250 element.get('luns').setValue(newImages);
253 this.groups.controls.forEach((element) => {
254 const newDisks = element.value.disks.filter((item) => item !== name);
255 element.get('disks').setValue(newDisks);
258 _.forEach(this.imagesInitiatorSelections, (selections, i) => {
259 this.imagesInitiatorSelections[i] = selections.filter((item: any) => item.name !== name);
261 _.forEach(this.groupDiskSelections, (selections, i) => {
262 this.groupDiskSelections[i] = selections.filter((item: any) => item.name !== name);
266 onImageSelection($event) {
267 const option = $event.option;
269 if (option.selected) {
270 if (!this.imagesSettings[option.name]) {
271 this.imagesSettings[option.name] = {};
274 _.forEach(this.imagesInitiatorSelections, (selections, i) => {
275 selections.push(new SelectOption(false, option.name, ''));
276 this.imagesInitiatorSelections[i] = [...selections];
279 _.forEach(this.groupDiskSelections, (selections, i) => {
280 selections.push(new SelectOption(false, option.name, ''));
281 this.groupDiskSelections[i] = [...selections];
284 this.removeImageRefs(option.name);
290 return this.targetForm.get('initiators') as FormArray;
294 const fg = new CdFormGroup({
295 client_iqn: new FormControl('', {
298 CdValidators.custom('notUnique', (client_iqn) => {
299 const flattened = this.initiators.controls.reduce(function(accumulator, currentValue) {
300 return accumulator.concat(currentValue.value.client_iqn);
303 return flattened.indexOf(client_iqn) !== flattened.lastIndexOf(client_iqn);
305 Validators.pattern(this.IQN_REGEX)
308 auth: new CdFormGroup({
309 user: new FormControl(''),
310 password: new FormControl(''),
311 mutual_user: new FormControl(''),
312 mutual_password: new FormControl('')
314 luns: new FormControl([]),
315 cdIsInGroup: new FormControl(false)
318 CdValidators.validateIf(
320 () => fg.getValue('password') || fg.getValue('mutual_user') || fg.getValue('mutual_password'),
321 [Validators.required],
322 [Validators.pattern(this.USER_REGEX)],
323 [fg.get('password'), fg.get('mutual_user'), fg.get('mutual_password')]
326 CdValidators.validateIf(
328 () => fg.getValue('user') || fg.getValue('mutual_user') || fg.getValue('mutual_password'),
329 [Validators.required],
330 [Validators.pattern(this.PASSWORD_REGEX)],
331 [fg.get('user'), fg.get('mutual_user'), fg.get('mutual_password')]
334 CdValidators.validateIf(
335 fg.get('mutual_user'),
336 () => fg.getValue('mutual_password'),
337 [Validators.required],
338 [Validators.pattern(this.USER_REGEX)],
339 [fg.get('user'), fg.get('password'), fg.get('mutual_password')]
342 CdValidators.validateIf(
343 fg.get('mutual_password'),
344 () => fg.getValue('mutual_user'),
345 [Validators.required],
346 [Validators.pattern(this.PASSWORD_REGEX)],
347 [fg.get('user'), fg.get('password'), fg.get('mutual_user')]
350 this.initiators.push(fg);
352 _.forEach(this.groupMembersSelections, (selections, i) => {
353 selections.push(new SelectOption(false, '', ''));
354 this.groupMembersSelections[i] = [...selections];
358 this.targetForm.getValue('disks'),
359 (disk) => new SelectOption(false, disk, '')
361 this.imagesInitiatorSelections.push(disks);
366 removeInitiator(index) {
367 const removed = this.initiators.value[index];
369 this.initiators.removeAt(index);
371 _.forEach(this.groupMembersSelections, (selections, i) => {
372 selections.splice(index, 1);
373 this.groupMembersSelections[i] = [...selections];
376 this.groups.controls.forEach((element) => {
377 const newMembers = element.value.members.filter((item) => item !== removed.client_iqn);
378 element.get('members').setValue(newMembers);
381 this.imagesInitiatorSelections.splice(index, 1);
384 updatedInitiatorSelector() {
385 // Validate all client_iqn
386 this.initiators.controls.forEach((control) => {
387 control.get('client_iqn').updateValueAndValidity({ emitEvent: false });
390 // Update Group Initiator Selector
391 _.forEach(this.groupMembersSelections, (group, group_index) => {
392 _.forEach(group, (elem, index) => {
393 const oldName = elem.name;
394 elem.name = this.initiators.controls[index].value.client_iqn;
396 this.groups.controls.forEach((element) => {
397 const members = element.value.members;
398 const i = members.indexOf(oldName);
401 members[i] = elem.name;
403 element.get('members').setValue(members);
406 this.groupMembersSelections[group_index] = [...this.groupMembersSelections[group_index]];
410 removeInitiatorImage(initiator: any, lun_index: number, initiator_index: string, image: string) {
411 const luns = initiator.getValue('luns');
412 luns.splice(lun_index, 1);
413 initiator.patchValue({ luns: luns });
415 this.imagesInitiatorSelections[initiator_index].forEach((value) => {
416 if (value.name === image) {
417 value.selected = false;
426 return this.targetForm.get('groups') as FormArray;
430 const fg = new CdFormGroup({
431 group_id: new FormControl('', { validators: [Validators.required] }),
432 members: new FormControl([]),
433 disks: new FormControl([])
436 this.groups.push(fg);
439 this.targetForm.getValue('disks'),
440 (disk) => new SelectOption(false, disk, '')
442 this.groupDiskSelections.push(disks);
444 const initiators = _.map(
445 this.initiators.value,
446 (initiator) => new SelectOption(false, initiator.client_iqn, '')
448 this.groupMembersSelections.push(initiators);
454 this.groups.removeAt(index);
455 this.groupDiskSelections.splice(index, 1);
458 onGroupMemberSelection($event) {
459 const option = $event.option;
461 this.initiators.controls.forEach((element) => {
462 if (element.value.client_iqn === option.name) {
463 element.patchValue({ luns: [] });
464 element.get('cdIsInGroup').setValue(option.selected);
469 removeGroupInitiator(group, member_index, group_index) {
470 const name = group.getValue('members')[member_index];
471 group.getValue('members').splice(member_index, 1);
473 this.groupMembersSelections[group_index].forEach((value) => {
474 if (value.name === name) {
475 value.selected = false;
478 this.groupMembersSelections[group_index] = [...this.groupMembersSelections[group_index]];
480 this.onGroupMemberSelection({ option: new SelectOption(false, name, '') });
483 removeGroupDisk(group, disk_index, group_index) {
484 const name = group.getValue('disks')[disk_index];
485 group.getValue('disks').splice(disk_index, 1);
487 this.groupDiskSelections[group_index].forEach((value) => {
488 if (value.name === name) {
489 value.selected = false;
492 this.groupDiskSelections[group_index] = [...this.groupDiskSelections[group_index]];
496 const formValue = this.targetForm.value;
499 target_iqn: this.targetForm.getValue('target_iqn'),
500 target_controls: this.targetForm.getValue('target_controls'),
508 formValue.disks.forEach((disk) => {
509 const imageSplit = disk.split('/');
512 image: imageSplit[1],
513 controls: this.imagesSettings[disk]
518 formValue.portals.forEach((portal) => {
519 const portalSplit = portal.split(':');
520 request.portals.push({
521 host: portalSplit[0],
527 formValue.initiators.forEach((initiator) => {
528 if (!initiator.auth.user) {
529 initiator.auth.user = null;
531 if (!initiator.auth.password) {
532 initiator.auth.password = null;
534 if (!initiator.auth.mutual_user) {
535 initiator.auth.mutual_user = null;
537 if (!initiator.auth.mutual_password) {
538 initiator.auth.mutual_password = null;
542 initiator.luns.forEach((lun) => {
543 const imageSplit = lun.split('/');
550 initiator.luns = newLuns;
552 request.clients = formValue.initiators;
555 formValue.groups.forEach((group) => {
557 group.disks.forEach((disk) => {
558 const imageSplit = disk.split('/');
565 group.disks = newDisks;
567 request.groups = formValue.groups;
571 request['new_target_iqn'] = request.target_iqn;
572 request.target_iqn = this.target_iqn;
573 wrapTask = this.taskWrapper.wrapTaskAroundCall({
574 task: new FinishedTask('iscsi/target/edit', {
575 target_iqn: request.target_iqn
577 call: this.iscsiService.updateTarget(this.target_iqn, request)
580 wrapTask = this.taskWrapper.wrapTaskAroundCall({
581 task: new FinishedTask('iscsi/target/create', {
582 target_iqn: request.target_iqn
584 call: this.iscsiService.createTarget(request)
591 this.targetForm.setErrors({ cdSubmitButton: true });
593 () => this.router.navigate(['/block/iscsi/targets'])
597 targetSettingsModal() {
598 const initialState = {
599 target_controls: this.targetForm.get('target_controls'),
600 target_default_controls: this.target_default_controls
603 this.modalRef = this.modalService.show(IscsiTargetIqnSettingsModalComponent, { initialState });
606 imageSettingsModal(image) {
607 const initialState = {
608 imagesSettings: this.imagesSettings,
610 disk_default_controls: this.disk_default_controls
613 this.modalRef = this.modalService.show(IscsiTargetImageSettingsModalComponent, {