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;
33 default_backstore: string;
39 imagesSelections: SelectOption[];
40 portalsSelections: SelectOption[] = [];
42 imagesInitiatorSelections: SelectOption[][] = [];
43 groupDiskSelections: SelectOption[][] = [];
44 groupMembersSelections: SelectOption[][] = [];
46 imagesSettings: any = {};
48 portals: new SelectMessages(
49 { noOptions: this.i18n('There are no portals available.') },
52 images: new SelectMessages(
53 { noOptions: this.i18n('There are no images available.') },
56 initiatorImage: new SelectMessages(
59 'There are no images available. Please make sure you add an image to the target.'
64 groupInitiator: new SelectMessages(
67 'There are no initiators available. Please make sure you add an initiator to the target.'
74 IQN_REGEX = /^iqn\.(19|20)\d\d-(0[1-9]|1[0-2])\.\D{2,3}(\.[A-Za-z0-9-]+)+(:[A-Za-z0-9-\.]+)*$/;
75 USER_REGEX = /[\w\.:@_-]{8,64}/;
76 PASSWORD_REGEX = /[\w@\-_]{12,16}/;
79 private iscsiService: IscsiService,
80 private modalService: BsModalService,
81 private rbdService: RbdService,
82 private router: Router,
83 private route: ActivatedRoute,
85 private taskWrapper: TaskWrapperService
89 const promises: any[] = [
90 this.iscsiService.listTargets(),
91 this.rbdService.list(),
92 this.iscsiService.portals(),
93 this.iscsiService.settings()
96 if (this.router.url.startsWith('/block/iscsi/targets/edit')) {
98 this.route.params.subscribe((params: { target_iqn: string }) => {
99 this.target_iqn = decodeURIComponent(params.target_iqn);
100 promises.push(this.iscsiService.getTarget(this.target_iqn));
104 forkJoin(promises).subscribe((data: any[]) => {
105 // iscsiService.listTargets
106 const usedImages = _(data[0])
107 .filter((target) => target.target_iqn !== this.target_iqn)
108 .flatMap((target) => target.disks)
109 .map((image) => `${image.pool}/${image.image}`)
113 this.imagesAll = _(data[1])
114 .flatMap((pool) => pool.value)
115 .map((image) => `${image.pool_name}/${image.name}`)
116 .filter((image) => usedImages.indexOf(image) === -1)
119 this.imagesSelections = this.imagesAll.map((image) => new SelectOption(false, image, ''));
121 // iscsiService.portals()
122 const portals: SelectOption[] = [];
123 data[2].forEach((portal) => {
124 portal.ip_addresses.forEach((ip) => {
125 portals.push(new SelectOption(false, portal.name + ':' + ip, ''));
128 this.portalsSelections = [...portals];
130 // iscsiService.settings()
131 this.minimum_gateways = data[3].config.minimum_gateways;
132 this.target_default_controls = data[3].target_default_controls;
133 this.disk_default_controls = data[3].disk_default_controls;
134 this.backstores = data[3].backstores;
135 this.default_backstore = data[3].default_backstore;
139 // iscsiService.getTarget()
141 this.resolveModel(data[4]);
147 this.targetForm = new CdFormGroup({
148 target_iqn: new FormControl('iqn.2001-07.com.ceph:' + Date.now(), {
149 validators: [Validators.required, Validators.pattern(this.IQN_REGEX)]
151 target_controls: new FormControl({}),
152 portals: new FormControl([], {
154 CdValidators.custom('minGateways', (value) => {
155 const gateways = _.uniq(value.map((elem) => elem.split(':')[0]));
156 return gateways.length < Math.max(1, this.minimum_gateways);
160 disks: new FormControl([]),
161 initiators: new FormArray([]),
162 groups: new FormArray([])
167 this.targetForm.patchValue({
168 target_iqn: res.target_iqn,
169 target_controls: res.target_controls
173 _.forEach(res.portals, (portal) => {
174 const id = `${portal.host}:${portal.ip}`;
177 this.targetForm.patchValue({
182 _.forEach(res.disks, (disk) => {
183 const id = `${disk.pool}/${disk.image}`;
185 this.imagesSettings[id] = {
186 backstore: disk.backstore
188 this.imagesSettings[id][disk.backstore] = disk.controls;
190 this.onImageSelection({ option: { name: id, selected: true } });
192 this.targetForm.patchValue({
196 _.forEach(res.clients, (client) => {
197 const initiator = this.addInitiator();
198 client.luns = _.map(client.luns, (lun) => `${lun.pool}/${lun.image}`);
199 initiator.patchValue(client);
200 // updatedInitiatorSelector()
203 _.forEach(res.groups, (group) => {
204 const fg = this.addGroup();
206 group.disks = _.map(group.disks, (disk) => `${disk.pool}/${disk.image}`);
207 fg.patchValue(group);
208 _.forEach(group.members, (member) => {
209 this.onGroupMemberSelection({ option: new SelectOption(true, member, '') });
214 hasAdvancedSettings(settings: any) {
215 return Object.values(settings).length > 0;
220 return this.targetForm.get('portals') as FormControl;
223 onPortalSelection() {
224 this.portals.setValue(this.portals.value);
227 removePortal(index: number, portal: string) {
228 this.portalsSelections.forEach((value) => {
229 if (value.name === portal) {
230 value.selected = false;
234 this.portals.value.splice(index, 1);
235 this.portals.setValue(this.portals.value);
241 return this.targetForm.get('disks') as FormControl;
244 removeImage(index: number, image: string) {
245 this.imagesSelections.forEach((value) => {
246 if (value.name === image) {
247 value.selected = false;
250 this.disks.value.splice(index, 1);
251 this.removeImageRefs(image);
255 removeImageRefs(name) {
256 this.initiators.controls.forEach((element) => {
257 const newImages = element.value.luns.filter((item) => item !== name);
258 element.get('luns').setValue(newImages);
261 this.groups.controls.forEach((element) => {
262 const newDisks = element.value.disks.filter((item) => item !== name);
263 element.get('disks').setValue(newDisks);
266 _.forEach(this.imagesInitiatorSelections, (selections, i) => {
267 this.imagesInitiatorSelections[i] = selections.filter((item: any) => item.name !== name);
269 _.forEach(this.groupDiskSelections, (selections, i) => {
270 this.groupDiskSelections[i] = selections.filter((item: any) => item.name !== name);
274 onImageSelection($event) {
275 const option = $event.option;
277 if (option.selected) {
278 if (!this.imagesSettings[option.name]) {
279 this.imagesSettings[option.name] = {
280 backstore: this.default_backstore
282 this.imagesSettings[option.name][this.default_backstore] = {};
285 _.forEach(this.imagesInitiatorSelections, (selections, i) => {
286 selections.push(new SelectOption(false, option.name, ''));
287 this.imagesInitiatorSelections[i] = [...selections];
290 _.forEach(this.groupDiskSelections, (selections, i) => {
291 selections.push(new SelectOption(false, option.name, ''));
292 this.groupDiskSelections[i] = [...selections];
295 this.removeImageRefs(option.name);
301 return this.targetForm.get('initiators') as FormArray;
305 const fg = new CdFormGroup({
306 client_iqn: new FormControl('', {
309 CdValidators.custom('notUnique', (client_iqn) => {
310 const flattened = this.initiators.controls.reduce(function(accumulator, currentValue) {
311 return accumulator.concat(currentValue.value.client_iqn);
314 return flattened.indexOf(client_iqn) !== flattened.lastIndexOf(client_iqn);
316 Validators.pattern(this.IQN_REGEX)
319 auth: new CdFormGroup({
320 user: new FormControl(''),
321 password: new FormControl(''),
322 mutual_user: new FormControl(''),
323 mutual_password: new FormControl('')
325 luns: new FormControl([]),
326 cdIsInGroup: new FormControl(false)
329 CdValidators.validateIf(
331 () => fg.getValue('password') || fg.getValue('mutual_user') || fg.getValue('mutual_password'),
332 [Validators.required],
333 [Validators.pattern(this.USER_REGEX)],
334 [fg.get('password'), fg.get('mutual_user'), fg.get('mutual_password')]
337 CdValidators.validateIf(
339 () => fg.getValue('user') || fg.getValue('mutual_user') || fg.getValue('mutual_password'),
340 [Validators.required],
341 [Validators.pattern(this.PASSWORD_REGEX)],
342 [fg.get('user'), fg.get('mutual_user'), fg.get('mutual_password')]
345 CdValidators.validateIf(
346 fg.get('mutual_user'),
347 () => fg.getValue('mutual_password'),
348 [Validators.required],
349 [Validators.pattern(this.USER_REGEX)],
350 [fg.get('user'), fg.get('password'), fg.get('mutual_password')]
353 CdValidators.validateIf(
354 fg.get('mutual_password'),
355 () => fg.getValue('mutual_user'),
356 [Validators.required],
357 [Validators.pattern(this.PASSWORD_REGEX)],
358 [fg.get('user'), fg.get('password'), fg.get('mutual_user')]
361 this.initiators.push(fg);
363 _.forEach(this.groupMembersSelections, (selections, i) => {
364 selections.push(new SelectOption(false, '', ''));
365 this.groupMembersSelections[i] = [...selections];
369 this.targetForm.getValue('disks'),
370 (disk) => new SelectOption(false, disk, '')
372 this.imagesInitiatorSelections.push(disks);
377 removeInitiator(index) {
378 const removed = this.initiators.value[index];
380 this.initiators.removeAt(index);
382 _.forEach(this.groupMembersSelections, (selections, i) => {
383 selections.splice(index, 1);
384 this.groupMembersSelections[i] = [...selections];
387 this.groups.controls.forEach((element) => {
388 const newMembers = element.value.members.filter((item) => item !== removed.client_iqn);
389 element.get('members').setValue(newMembers);
392 this.imagesInitiatorSelections.splice(index, 1);
395 updatedInitiatorSelector() {
396 // Validate all client_iqn
397 this.initiators.controls.forEach((control) => {
398 control.get('client_iqn').updateValueAndValidity({ emitEvent: false });
401 // Update Group Initiator Selector
402 _.forEach(this.groupMembersSelections, (group, group_index) => {
403 _.forEach(group, (elem, index) => {
404 const oldName = elem.name;
405 elem.name = this.initiators.controls[index].value.client_iqn;
407 this.groups.controls.forEach((element) => {
408 const members = element.value.members;
409 const i = members.indexOf(oldName);
412 members[i] = elem.name;
414 element.get('members').setValue(members);
417 this.groupMembersSelections[group_index] = [...this.groupMembersSelections[group_index]];
421 removeInitiatorImage(initiator: any, lun_index: number, initiator_index: string, image: string) {
422 const luns = initiator.getValue('luns');
423 luns.splice(lun_index, 1);
424 initiator.patchValue({ luns: luns });
426 this.imagesInitiatorSelections[initiator_index].forEach((value) => {
427 if (value.name === image) {
428 value.selected = false;
437 return this.targetForm.get('groups') as FormArray;
441 const fg = new CdFormGroup({
442 group_id: new FormControl('', { validators: [Validators.required] }),
443 members: new FormControl([]),
444 disks: new FormControl([])
447 this.groups.push(fg);
450 this.targetForm.getValue('disks'),
451 (disk) => new SelectOption(false, disk, '')
453 this.groupDiskSelections.push(disks);
455 const initiators = _.map(
456 this.initiators.value,
457 (initiator) => new SelectOption(false, initiator.client_iqn, '')
459 this.groupMembersSelections.push(initiators);
465 this.groups.removeAt(index);
466 this.groupDiskSelections.splice(index, 1);
469 onGroupMemberSelection($event) {
470 const option = $event.option;
472 this.initiators.controls.forEach((element) => {
473 if (element.value.client_iqn === option.name) {
474 element.patchValue({ luns: [] });
475 element.get('cdIsInGroup').setValue(option.selected);
480 removeGroupInitiator(group, member_index, group_index) {
481 const name = group.getValue('members')[member_index];
482 group.getValue('members').splice(member_index, 1);
484 this.groupMembersSelections[group_index].forEach((value) => {
485 if (value.name === name) {
486 value.selected = false;
489 this.groupMembersSelections[group_index] = [...this.groupMembersSelections[group_index]];
491 this.onGroupMemberSelection({ option: new SelectOption(false, name, '') });
494 removeGroupDisk(group, disk_index, group_index) {
495 const name = group.getValue('disks')[disk_index];
496 group.getValue('disks').splice(disk_index, 1);
498 this.groupDiskSelections[group_index].forEach((value) => {
499 if (value.name === name) {
500 value.selected = false;
503 this.groupDiskSelections[group_index] = [...this.groupDiskSelections[group_index]];
507 const formValue = this.targetForm.value;
510 target_iqn: this.targetForm.getValue('target_iqn'),
511 target_controls: this.targetForm.getValue('target_controls'),
519 formValue.disks.forEach((disk) => {
520 const imageSplit = disk.split('/');
521 const backstore = this.imagesSettings[disk].backstore;
524 image: imageSplit[1],
525 backstore: backstore,
526 controls: this.imagesSettings[disk][backstore]
531 formValue.portals.forEach((portal) => {
532 const portalSplit = portal.split(':');
533 request.portals.push({
534 host: portalSplit[0],
540 formValue.initiators.forEach((initiator) => {
541 if (!initiator.auth.user) {
542 initiator.auth.user = null;
544 if (!initiator.auth.password) {
545 initiator.auth.password = null;
547 if (!initiator.auth.mutual_user) {
548 initiator.auth.mutual_user = null;
550 if (!initiator.auth.mutual_password) {
551 initiator.auth.mutual_password = null;
555 initiator.luns.forEach((lun) => {
556 const imageSplit = lun.split('/');
563 initiator.luns = newLuns;
565 request.clients = formValue.initiators;
568 formValue.groups.forEach((group) => {
570 group.disks.forEach((disk) => {
571 const imageSplit = disk.split('/');
578 group.disks = newDisks;
580 request.groups = formValue.groups;
584 request['new_target_iqn'] = request.target_iqn;
585 request.target_iqn = this.target_iqn;
586 wrapTask = this.taskWrapper.wrapTaskAroundCall({
587 task: new FinishedTask('iscsi/target/edit', {
588 target_iqn: request.target_iqn
590 call: this.iscsiService.updateTarget(this.target_iqn, request)
593 wrapTask = this.taskWrapper.wrapTaskAroundCall({
594 task: new FinishedTask('iscsi/target/create', {
595 target_iqn: request.target_iqn
597 call: this.iscsiService.createTarget(request)
604 this.targetForm.setErrors({ cdSubmitButton: true });
606 () => this.router.navigate(['/block/iscsi/targets'])
610 targetSettingsModal() {
611 const initialState = {
612 target_controls: this.targetForm.get('target_controls'),
613 target_default_controls: this.target_default_controls
616 this.modalRef = this.modalService.show(IscsiTargetIqnSettingsModalComponent, { initialState });
619 imageSettingsModal(image) {
620 const initialState = {
621 imagesSettings: this.imagesSettings,
623 disk_default_controls: this.disk_default_controls,
624 backstores: this.backstores
627 this.modalRef = this.modalService.show(IscsiTargetImageSettingsModalComponent, {