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 { ActionLabelsI18n } from '../../../shared/constants/app.constants';
15 import { Icons } from '../../../shared/enum/icons.enum';
16 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
17 import { CdValidators } from '../../../shared/forms/cd-validators';
18 import { FinishedTask } from '../../../shared/models/finished-task';
19 import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
20 import { IscsiTargetImageSettingsModalComponent } from '../iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component';
21 import { IscsiTargetIqnSettingsModalComponent } from '../iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component';
24 selector: 'cd-iscsi-target-form',
25 templateUrl: './iscsi-target-form.component.html',
26 styleUrls: ['./iscsi-target-form.component.scss']
28 export class IscsiTargetFormComponent implements OnInit {
29 targetForm: CdFormGroup;
32 target_default_controls: any;
33 target_controls_limits: any;
34 disk_default_controls: any;
35 disk_controls_limits: any;
37 default_backstore: string;
38 unsupported_rbd_features: any;
39 required_rbd_features: any;
47 imagesSelections: SelectOption[];
48 portalsSelections: SelectOption[] = [];
50 imagesInitiatorSelections: SelectOption[][] = [];
51 groupDiskSelections: SelectOption[][] = [];
52 groupMembersSelections: SelectOption[][] = [];
54 imagesSettings: any = {};
56 portals: new SelectMessages(
57 { noOptions: this.i18n('There are no portals available.') },
60 images: new SelectMessages(
61 { noOptions: this.i18n('There are no images available.') },
64 initiatorImage: new SelectMessages(
67 'There are no images available. Please make sure you add an image to the target.'
72 groupInitiator: new SelectMessages(
75 'There are no initiators available. Please make sure you add an initiator to the target.'
82 IQN_REGEX = /^iqn\.(19|20)\d\d-(0[1-9]|1[0-2])\.\D{2,3}(\.[A-Za-z0-9-]+)+(:[A-Za-z0-9-\.]+)*$/;
83 USER_REGEX = /[\w\.:@_-]{8,64}/;
84 PASSWORD_REGEX = /[\w@\-_\/]{12,16}/;
89 private iscsiService: IscsiService,
90 private modalService: BsModalService,
91 private rbdService: RbdService,
92 private router: Router,
93 private route: ActivatedRoute,
95 private taskWrapper: TaskWrapperService,
96 public actionLabels: ActionLabelsI18n
98 this.resource = this.i18n('target');
102 const promises: any[] = [
103 this.iscsiService.listTargets(),
104 this.rbdService.list(),
105 this.iscsiService.portals(),
106 this.iscsiService.settings()
109 if (this.router.url.startsWith('/block/iscsi/targets/edit')) {
111 this.route.params.subscribe((params: { target_iqn: string }) => {
112 this.target_iqn = decodeURIComponent(params.target_iqn);
113 promises.push(this.iscsiService.getTarget(this.target_iqn));
116 this.action = this.isEdit ? this.actionLabels.EDIT : this.actionLabels.CREATE;
118 forkJoin(promises).subscribe((data: any[]) => {
119 // iscsiService.listTargets
120 const usedImages = _(data[0])
121 .filter((target) => target.target_iqn !== this.target_iqn)
122 .flatMap((target) => target.disks)
123 .map((image) => `${image.pool}/${image.image}`)
126 // iscsiService.settings()
127 this.minimum_gateways = data[3].config.minimum_gateways;
128 this.target_default_controls = data[3].target_default_controls;
129 this.target_controls_limits = data[3].target_controls_limits;
130 this.disk_default_controls = data[3].disk_default_controls;
131 this.disk_controls_limits = data[3].disk_controls_limits;
132 this.backstores = data[3].backstores;
133 this.default_backstore = data[3].default_backstore;
134 this.unsupported_rbd_features = data[3].unsupported_rbd_features;
135 this.required_rbd_features = data[3].required_rbd_features;
138 this.imagesAll = _(data[1])
139 .flatMap((pool) => pool.value)
141 const imageId = `${image.pool_name}/${image.name}`;
142 if (usedImages.indexOf(imageId) !== -1) {
145 const validBackstores = this.getValidBackstores(image);
146 if (validBackstores.length === 0) {
153 this.imagesSelections = this.imagesAll.map(
154 (image) => new SelectOption(false, `${image.pool_name}/${image.name}`, '')
157 // iscsiService.portals()
158 const portals: SelectOption[] = [];
159 data[2].forEach((portal) => {
160 portal.ip_addresses.forEach((ip) => {
161 portals.push(new SelectOption(false, portal.name + ':' + ip, ''));
164 this.portalsSelections = [...portals];
168 // iscsiService.getTarget()
170 this.resolveModel(data[4]);
176 this.targetForm = new CdFormGroup({
177 target_iqn: new FormControl('iqn.2001-07.com.ceph:' + Date.now(), {
178 validators: [Validators.required, Validators.pattern(this.IQN_REGEX)]
180 target_controls: new FormControl({}),
181 portals: new FormControl([], {
183 CdValidators.custom('minGateways', (value) => {
184 const gateways = _.uniq(value.map((elem) => elem.split(':')[0]));
185 return gateways.length < Math.max(1, this.minimum_gateways);
189 disks: new FormControl([]),
190 initiators: new FormArray([]),
191 groups: new FormArray([]),
192 acl_enabled: new FormControl(false)
197 this.targetForm.patchValue({
198 target_iqn: res.target_iqn,
199 target_controls: res.target_controls,
200 acl_enabled: res.acl_enabled
204 _.forEach(res.portals, (portal) => {
205 const id = `${portal.host}:${portal.ip}`;
208 this.targetForm.patchValue({
213 _.forEach(res.disks, (disk) => {
214 const id = `${disk.pool}/${disk.image}`;
216 this.imagesSettings[id] = {
217 backstore: disk.backstore
219 this.imagesSettings[id][disk.backstore] = disk.controls;
221 this.onImageSelection({ option: { name: id, selected: true } });
223 this.targetForm.patchValue({
227 _.forEach(res.clients, (client) => {
228 const initiator = this.addInitiator();
229 client.luns = _.map(client.luns, (lun) => `${lun.pool}/${lun.image}`);
230 initiator.patchValue(client);
231 // updatedInitiatorSelector()
234 _.forEach(res.groups, (group) => {
235 const fg = this.addGroup();
236 group.disks = _.map(group.disks, (disk) => `${disk.pool}/${disk.image}`);
237 fg.patchValue(group);
238 _.forEach(group.members, (member) => {
239 this.onGroupMemberSelection({ option: new SelectOption(true, member, '') });
244 hasAdvancedSettings(settings: any) {
245 return Object.values(settings).length > 0;
250 return this.targetForm.get('portals') as FormControl;
253 onPortalSelection() {
254 this.portals.setValue(this.portals.value);
257 removePortal(index: number, portal: string) {
258 this.portalsSelections.forEach((value) => {
259 if (value.name === portal) {
260 value.selected = false;
264 this.portals.value.splice(index, 1);
265 this.portals.setValue(this.portals.value);
271 return this.targetForm.get('disks') as FormControl;
274 removeImage(index: number, image: string) {
275 this.imagesSelections.forEach((value) => {
276 if (value.name === image) {
277 value.selected = false;
280 this.disks.value.splice(index, 1);
281 this.removeImageRefs(image);
285 removeImageRefs(name) {
286 this.initiators.controls.forEach((element) => {
287 const newImages = element.value.luns.filter((item) => item !== name);
288 element.get('luns').setValue(newImages);
291 this.groups.controls.forEach((element) => {
292 const newDisks = element.value.disks.filter((item) => item !== name);
293 element.get('disks').setValue(newDisks);
296 _.forEach(this.imagesInitiatorSelections, (selections, i) => {
297 this.imagesInitiatorSelections[i] = selections.filter((item: any) => item.name !== name);
299 _.forEach(this.groupDiskSelections, (selections, i) => {
300 this.groupDiskSelections[i] = selections.filter((item: any) => item.name !== name);
304 getDefaultBackstore(imageId) {
305 let result = this.default_backstore;
306 const image = this.getImageById(imageId);
307 if (!this.validFeatures(image, this.default_backstore)) {
308 this.backstores.forEach((backstore) => {
309 if (backstore !== this.default_backstore) {
310 if (this.validFeatures(image, backstore)) {
319 onImageSelection($event) {
320 const option = $event.option;
322 if (option.selected) {
323 if (!this.imagesSettings[option.name]) {
324 const defaultBackstore = this.getDefaultBackstore(option.name);
325 this.imagesSettings[option.name] = {
326 backstore: defaultBackstore
328 this.imagesSettings[option.name][defaultBackstore] = {};
331 _.forEach(this.imagesInitiatorSelections, (selections, i) => {
332 selections.push(new SelectOption(false, option.name, ''));
333 this.imagesInitiatorSelections[i] = [...selections];
336 _.forEach(this.groupDiskSelections, (selections, i) => {
337 selections.push(new SelectOption(false, option.name, ''));
338 this.groupDiskSelections[i] = [...selections];
341 this.removeImageRefs(option.name);
347 return this.targetForm.get('initiators') as FormArray;
351 const fg = new CdFormGroup({
352 client_iqn: new FormControl('', {
355 CdValidators.custom('notUnique', (client_iqn) => {
356 const flattened = this.initiators.controls.reduce(function(accumulator, currentValue) {
357 return accumulator.concat(currentValue.value.client_iqn);
360 return flattened.indexOf(client_iqn) !== flattened.lastIndexOf(client_iqn);
362 Validators.pattern(this.IQN_REGEX)
365 auth: new CdFormGroup({
366 user: new FormControl(''),
367 password: new FormControl(''),
368 mutual_user: new FormControl(''),
369 mutual_password: new FormControl('')
371 luns: new FormControl([]),
372 cdIsInGroup: new FormControl(false)
375 CdValidators.validateIf(
377 () => fg.getValue('password') || fg.getValue('mutual_user') || fg.getValue('mutual_password'),
378 [Validators.required],
379 [Validators.pattern(this.USER_REGEX)],
380 [fg.get('password'), fg.get('mutual_user'), fg.get('mutual_password')]
383 CdValidators.validateIf(
385 () => fg.getValue('user') || fg.getValue('mutual_user') || fg.getValue('mutual_password'),
386 [Validators.required],
387 [Validators.pattern(this.PASSWORD_REGEX)],
388 [fg.get('user'), fg.get('mutual_user'), fg.get('mutual_password')]
391 CdValidators.validateIf(
392 fg.get('mutual_user'),
393 () => fg.getValue('mutual_password'),
394 [Validators.required],
395 [Validators.pattern(this.USER_REGEX)],
396 [fg.get('user'), fg.get('password'), fg.get('mutual_password')]
399 CdValidators.validateIf(
400 fg.get('mutual_password'),
401 () => fg.getValue('mutual_user'),
402 [Validators.required],
403 [Validators.pattern(this.PASSWORD_REGEX)],
404 [fg.get('user'), fg.get('password'), fg.get('mutual_user')]
407 this.initiators.push(fg);
409 _.forEach(this.groupMembersSelections, (selections, i) => {
410 selections.push(new SelectOption(false, '', ''));
411 this.groupMembersSelections[i] = [...selections];
415 this.targetForm.getValue('disks'),
416 (disk) => new SelectOption(false, disk, '')
418 this.imagesInitiatorSelections.push(disks);
423 removeInitiator(index) {
424 const removed = this.initiators.value[index];
426 this.initiators.removeAt(index);
428 _.forEach(this.groupMembersSelections, (selections, i) => {
429 selections.splice(index, 1);
430 this.groupMembersSelections[i] = [...selections];
433 this.groups.controls.forEach((element) => {
434 const newMembers = element.value.members.filter((item) => item !== removed.client_iqn);
435 element.get('members').setValue(newMembers);
438 this.imagesInitiatorSelections.splice(index, 1);
441 updatedInitiatorSelector() {
442 // Validate all client_iqn
443 this.initiators.controls.forEach((control) => {
444 control.get('client_iqn').updateValueAndValidity({ emitEvent: false });
447 // Update Group Initiator Selector
448 _.forEach(this.groupMembersSelections, (group, group_index) => {
449 _.forEach(group, (elem, index) => {
450 const oldName = elem.name;
451 elem.name = this.initiators.controls[index].value.client_iqn;
453 this.groups.controls.forEach((element) => {
454 const members = element.value.members;
455 const i = members.indexOf(oldName);
458 members[i] = elem.name;
460 element.get('members').setValue(members);
463 this.groupMembersSelections[group_index] = [...this.groupMembersSelections[group_index]];
467 removeInitiatorImage(initiator: any, lun_index: number, initiator_index: string, image: string) {
468 const luns = initiator.getValue('luns');
469 luns.splice(lun_index, 1);
470 initiator.patchValue({ luns: luns });
472 this.imagesInitiatorSelections[initiator_index].forEach((value) => {
473 if (value.name === image) {
474 value.selected = false;
483 return this.targetForm.get('groups') as FormArray;
487 const fg = new CdFormGroup({
488 group_id: new FormControl('', { validators: [Validators.required] }),
489 members: new FormControl([]),
490 disks: new FormControl([])
493 this.groups.push(fg);
496 this.targetForm.getValue('disks'),
497 (disk) => new SelectOption(false, disk, '')
499 this.groupDiskSelections.push(disks);
501 const initiators = _.map(
502 this.initiators.value,
503 (initiator) => new SelectOption(false, initiator.client_iqn, '', !initiator.cdIsInGroup)
505 this.groupMembersSelections.push(initiators);
511 this.groups.removeAt(index);
512 this.groupDiskSelections.splice(index, 1);
515 onGroupMemberSelection($event) {
516 const option = $event.option;
518 let initiator_index: number;
519 this.initiators.controls.forEach((element, index) => {
520 if (element.value.client_iqn === option.name) {
521 element.patchValue({ luns: [] });
522 element.get('cdIsInGroup').setValue(option.selected);
523 initiator_index = index;
527 // Members can only be at one group at a time, so when a member is selected
528 // in one group we need to disable its selection in other groups
529 _.forEach(this.groupMembersSelections, (group) => {
530 group[initiator_index].enabled = !option.selected;
534 removeGroupInitiator(group, member_index, group_index) {
535 const name = group.getValue('members')[member_index];
536 group.getValue('members').splice(member_index, 1);
538 this.groupMembersSelections[group_index].forEach((value) => {
539 if (value.name === name) {
540 value.selected = false;
543 this.groupMembersSelections[group_index] = [...this.groupMembersSelections[group_index]];
545 this.onGroupMemberSelection({ option: new SelectOption(false, name, '') });
548 removeGroupDisk(group, disk_index, group_index) {
549 const name = group.getValue('disks')[disk_index];
550 group.getValue('disks').splice(disk_index, 1);
552 this.groupDiskSelections[group_index].forEach((value) => {
553 if (value.name === name) {
554 value.selected = false;
557 this.groupDiskSelections[group_index] = [...this.groupDiskSelections[group_index]];
561 const formValue = _.cloneDeep(this.targetForm.value);
564 target_iqn: this.targetForm.getValue('target_iqn'),
565 target_controls: this.targetForm.getValue('target_controls'),
566 acl_enabled: this.targetForm.getValue('acl_enabled'),
574 formValue.disks.forEach((disk) => {
575 const imageSplit = disk.split('/');
576 const backstore = this.imagesSettings[disk].backstore;
579 image: imageSplit[1],
580 backstore: backstore,
581 controls: this.imagesSettings[disk][backstore]
586 formValue.portals.forEach((portal) => {
587 const index = portal.indexOf(':');
588 request.portals.push({
589 host: portal.substring(0, index),
590 ip: portal.substring(index + 1)
595 if (request.acl_enabled) {
596 formValue.initiators.forEach((initiator) => {
597 if (!initiator.auth.user) {
598 initiator.auth.user = '';
600 if (!initiator.auth.password) {
601 initiator.auth.password = '';
603 if (!initiator.auth.mutual_user) {
604 initiator.auth.mutual_user = '';
606 if (!initiator.auth.mutual_password) {
607 initiator.auth.mutual_password = '';
609 delete initiator.cdIsInGroup;
612 initiator.luns.forEach((lun) => {
613 const imageSplit = lun.split('/');
620 initiator.luns = newLuns;
622 request.clients = formValue.initiators;
626 if (request.acl_enabled) {
627 formValue.groups.forEach((group) => {
629 group.disks.forEach((disk) => {
630 const imageSplit = disk.split('/');
637 group.disks = newDisks;
639 request.groups = formValue.groups;
644 request['new_target_iqn'] = request.target_iqn;
645 request.target_iqn = this.target_iqn;
646 wrapTask = this.taskWrapper.wrapTaskAroundCall({
647 task: new FinishedTask('iscsi/target/edit', {
648 target_iqn: request.target_iqn
650 call: this.iscsiService.updateTarget(this.target_iqn, request)
653 wrapTask = this.taskWrapper.wrapTaskAroundCall({
654 task: new FinishedTask('iscsi/target/create', {
655 target_iqn: request.target_iqn
657 call: this.iscsiService.createTarget(request)
664 this.targetForm.setErrors({ cdSubmitButton: true });
666 () => this.router.navigate(['/block/iscsi/targets'])
670 targetSettingsModal() {
671 const initialState = {
672 target_controls: this.targetForm.get('target_controls'),
673 target_default_controls: this.target_default_controls,
674 target_controls_limits: this.target_controls_limits
677 this.modalRef = this.modalService.show(IscsiTargetIqnSettingsModalComponent, { initialState });
680 imageSettingsModal(image) {
681 const initialState = {
682 imagesSettings: this.imagesSettings,
684 disk_default_controls: this.disk_default_controls,
685 disk_controls_limits: this.disk_controls_limits,
686 backstores: this.getValidBackstores(this.getImageById(image))
689 this.modalRef = this.modalService.show(IscsiTargetImageSettingsModalComponent, {
694 validFeatures(image, backstore) {
695 const imageFeatures = image.features;
696 const requiredFeatures = this.required_rbd_features[backstore];
697 const unsupportedFeatures = this.unsupported_rbd_features[backstore];
698 // tslint:disable-next-line:no-bitwise
699 const validRequiredFeatures = (imageFeatures & requiredFeatures) === requiredFeatures;
700 // tslint:disable-next-line:no-bitwise
701 const validSupportedFeatures = (imageFeatures & unsupportedFeatures) === 0;
702 return validRequiredFeatures && validSupportedFeatures;
705 getImageById(imageId) {
706 return this.imagesAll.find((image) => imageId === `${image.pool_name}/${image.name}`);
709 getValidBackstores(image) {
710 return this.backstores.filter((backstore) => this.validFeatures(image, backstore));